space-data-module-sdk 0.8.5 → 0.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +13 -0
  2. package/docs/AGENTS.md +5 -0
  3. package/docs/browser-wasmedge-isomorphic.md +64 -0
  4. package/docs/provider-access-abi.md +600 -0
  5. package/package.json +3 -2
  6. package/schemas/orbpro/Propagator.fbs +19 -3
  7. package/src/compiler/compileModule.js +24 -1
  8. package/src/flow/flowCompiler.js +141 -2
  9. package/src/flow/flowRuntimeHost.js +7 -1
  10. package/src/flow/vendor/sdn-flow/MethodRegistry.js +6 -1
  11. package/src/generated/orbpro/propagator/propagator-source-description.js +2 -2
  12. package/src/generated/orbpro/propagator/propagator-source-description.ts +2 -2
  13. package/src/generated/orbpro/propagator/propagator-source-kind.js +13 -2
  14. package/src/generated/orbpro/propagator/propagator-source-kind.ts +13 -2
  15. package/src/generated/spacedatastandards/plg/pluginCategory.d.ts +18 -1
  16. package/src/generated/spacedatastandards/plg/pluginCategory.d.ts.map +1 -1
  17. package/src/generated/spacedatastandards/plg/pluginCategory.js +17 -0
  18. package/src/generated/spacedatastandards/plg/pluginCategory.ts +21 -1
  19. package/src/host/index.js +6 -0
  20. package/src/host/providerAccess.js +727 -0
  21. package/src/host/providerAccessAbi.js +403 -0
  22. package/src/host/providerAccessEngineAdapter.js +338 -0
  23. package/src/host/providerAccessFixtureAdapter.js +444 -0
  24. package/src/host/providerAccessTileStoreAdapter.js +366 -0
  25. package/src/host/terrainSourceSeam.js +205 -0
  26. package/src/host/wasiThreadHost.js +12 -1
  27. package/src/index.d.ts +39 -4
  28. package/src/testing/browserModuleHarness.js +48 -0
  29. package/src/testing/index.d.ts +12 -1
  30. package/src/testing/parityBrowserRunner.js +8 -1
  31. package/src/testing/workerModuleHarness.js +36 -7
  32. package/src/testing/workerModuleHarnessWorker.js +5 -4
  33. package/src/transport/pki.js +57 -7
  34. package/templates/provider-access-module/include/space_data_provider_abi.h +227 -0
@@ -196,6 +196,37 @@ const DEFAULT_IMPORTED_MEMORY_MAXIMUM_BYTES = 2 * 1024 * 1024 * 1024;
196
196
  const DEFAULT_DIRECT_INVOKE_REQUEST_ARENA_BYTES = 64 * 1024;
197
197
  const SHARED_ARRAY_BUFFER_TAG = "[object SharedArrayBuffer]";
198
198
 
199
+ /**
200
+ * Overwrite a plaintext wasm buffer in place. Best-effort by contract: a
201
+ * detached (transferred) buffer, a frozen typed array, or a runtime without
202
+ * writable views must not turn a successful module load into a thrown error.
203
+ *
204
+ * @param {Uint8Array|ArrayBuffer|ArrayBufferView|null|undefined} bytes
205
+ * @returns {number} bytes actually overwritten
206
+ */
207
+ export function zeroWasmBytes(bytes) {
208
+ if (!bytes) {
209
+ return 0;
210
+ }
211
+ try {
212
+ const view =
213
+ bytes instanceof Uint8Array
214
+ ? bytes
215
+ : bytes instanceof ArrayBuffer
216
+ ? new Uint8Array(bytes)
217
+ : ArrayBuffer.isView(bytes)
218
+ ? new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength)
219
+ : null;
220
+ if (!view || view.byteLength === 0) {
221
+ return 0;
222
+ }
223
+ view.fill(0);
224
+ return view.byteLength;
225
+ } catch {
226
+ return 0;
227
+ }
228
+ }
229
+
199
230
  export function isSharedArrayBufferLike(value) {
200
231
  return (
201
232
  value !== null &&
@@ -408,6 +439,11 @@ async function instantiateBrowserModule(options = {}) {
408
439
  export async function createBrowserModuleHarness(options = {}) {
409
440
  let wasmSource = options.wasmSource;
410
441
  const signaturePolicy = resolveModuleSignaturePolicy(options);
442
+ // Bytes THIS function materializes itself (Response/URL fetch, or the
443
+ // ArrayBuffer/array copy taken for signature verification) — never a
444
+ // caller-owned buffer, which this function neither created nor may safely
445
+ // scrub. Zeroed below once the module is compiled.
446
+ let ownedArtifactBytes = null;
411
447
  if (signaturePolicy) {
412
448
  if (wasmSource instanceof WebAssembly.Module) {
413
449
  throw new ModuleSignatureError(
@@ -434,8 +470,20 @@ export async function createBrowserModuleHarness(options = {}) {
434
470
  }
435
471
  await verifyModuleArtifact(artifactBytes, signaturePolicy);
436
472
  wasmSource = artifactBytes;
473
+ ownedArtifactBytes = artifactBytes;
437
474
  }
438
475
  const wasmModule = await compileWasmModule(wasmSource);
476
+ // Plaintext hygiene (wasm-plaintext-memory-hygiene): nothing needs the
477
+ // source bytes past this point. Scrub whatever THIS function materialized
478
+ // itself, then drop every local/options reference to the source — a
479
+ // caller-supplied Uint8Array/ArrayBuffer must not outlive this call by
480
+ // being pinned via closure over `options` for the harness's entire
481
+ // lifetime (the returned harness closes over `options` for many other,
482
+ // unrelated fields).
483
+ zeroWasmBytes(ownedArtifactBytes);
484
+ ownedArtifactBytes = null;
485
+ wasmSource = null;
486
+ options.wasmSource = null;
439
487
  const moduleImports = WebAssembly.Module.imports(wasmModule);
440
488
  const needsHostBridge = moduleImports.some(
441
489
  (entry) => entry.module === DEFAULT_HOSTCALL_IMPORT_MODULE,
@@ -439,9 +439,11 @@ export function resolveModuleHarnessLaunchPlan(options: {
439
439
  export function detectArtifactProfile(wasmModule: WebAssembly.Module): string;
440
440
 
441
441
  export function createBrowserModuleHarness(options?: {
442
- wasmSource: Uint8Array | ArrayBuffer | string | WebAssembly.Module | unknown;
442
+ wasmSource: Uint8Array | ArrayBuffer | string | Response | WebAssembly.Module | unknown;
443
+ verifySignature?: boolean | { trustedPublicKeys?: string[]; requireSignature?: boolean };
443
444
  host?: unknown;
444
445
  hostOptions?: unknown;
446
+ hostcallDispatch?: (operation: string, params: unknown) => unknown;
445
447
  args?: string[];
446
448
  env?: Record<string, string>;
447
449
  manifest?: Record<string, unknown>;
@@ -456,6 +458,15 @@ export function createBrowserModuleHarness(options?: {
456
458
  allowRawInvoke?: boolean;
457
459
  initialMemoryBytes?: number;
458
460
  maximumMemoryBytes?: number;
461
+ maxThreads?: number;
462
+ threadHostcallChannel?: {
463
+ channelName: string;
464
+ token: string;
465
+ maxResponseBytes?: number;
466
+ timeoutMs?: number;
467
+ };
468
+ enableBrowserWasiThreads?: boolean;
469
+ directInvokeRequestArenaBytes?: number;
459
470
  logOutput?: boolean;
460
471
  }): Promise<BrowserModuleHarness>;
461
472
 
@@ -81,8 +81,15 @@ export async function runParityPlanInBrowser({ baseUrl = "" } = {}) {
81
81
  let stderr = new Uint8Array(0);
82
82
  let harness = null;
83
83
  try {
84
+ // Plaintext hygiene: no per-case copy. createBrowserModuleHarness
85
+ // compiles directly from a caller-supplied buffer without copying it
86
+ // (and never mutates/zeroes a buffer it does not own — see
87
+ // browserModuleHarness.js's zeroWasmBytes/ownedArtifactBytes
88
+ // contract), so the one fetched `moduleBytes` is safe to reuse
89
+ // as-is across every case/thread-count iteration. A `.slice()` here
90
+ // would only add a fresh, unzeroed plaintext copy per run.
84
91
  harness = await createBrowserModuleHarness({
85
- wasmSource: moduleBytes.slice(),
92
+ wasmSource: moduleBytes,
86
93
  surface: "command",
87
94
  args: planCase.args,
88
95
  env: {
@@ -75,17 +75,46 @@ async function spawnWorker(workerUrl) {
75
75
  };
76
76
  }
77
77
 
78
- function toBytes(source, label) {
79
- if (source instanceof Uint8Array) return new Uint8Array(source);
80
- if (source instanceof ArrayBuffer) return new Uint8Array(source.slice(0));
78
+ /**
79
+ * Compile `source` to a `WebAssembly.Module` on THIS (controlling) thread
80
+ * before ever touching a worker.
81
+ *
82
+ * Plaintext-hygiene contract (mirrors OrbPro's `wasmModuleHygiene.js`,
83
+ * `wasm-plaintext-memory-hygiene`): a compiled `WebAssembly.Module` is
84
+ * structured-cloneable to a Worker (browser) or `node:worker_threads`
85
+ * (Node) — proven by `createWasiThreadSpawn` in `src/host/wasiThreadHost.js`,
86
+ * which hands the same compiled module to every pooled worker in both
87
+ * lanes. Shipping the MODULE instead of raw bytes means there is no second
88
+ * plaintext copy alive in the worker, and no re-compile once it arrives —
89
+ * `createBrowserModuleHarness`'s own `compileWasmModule` already accepts a
90
+ * `WebAssembly.Module` directly and returns it unchanged.
91
+ *
92
+ * `source` is never retained past this call and never zeroed here: a
93
+ * caller-supplied `Uint8Array`/`ArrayBuffer` is not this harness's buffer to
94
+ * mutate (compile does not retain a reference to it either — the caller is
95
+ * free to reuse or scrub it immediately after this resolves).
96
+ *
97
+ * @param {WebAssembly.Module|Uint8Array|ArrayBuffer} source
98
+ * @param {string} label diagnostic label
99
+ * @returns {Promise<WebAssembly.Module>}
100
+ */
101
+ async function toWasmModule(source, label) {
102
+ if (source instanceof WebAssembly.Module) {
103
+ return source;
104
+ }
105
+ if (source instanceof Uint8Array || source instanceof ArrayBuffer) {
106
+ return WebAssembly.compile(source);
107
+ }
81
108
  throw new TypeError(
82
- `${label} must be a Uint8Array or ArrayBuffer (the worker harness ships bytes to the worker).`,
109
+ `${label} must be a WebAssembly.Module, Uint8Array, or ArrayBuffer.`,
83
110
  );
84
111
  }
85
112
 
86
113
  /**
87
114
  * @param {object} options
88
- * @param {Uint8Array|ArrayBuffer} options.wasmSource module bytes
115
+ * @param {WebAssembly.Module|Uint8Array|ArrayBuffer} options.wasmSource module
116
+ * bytes OR an already-compiled module (preferred — skips a redundant
117
+ * compile and avoids ever materializing plaintext bytes in this harness)
89
118
  * @param {object} [options.host] host servicing guest hostcalls (default createBrowserHost(hostOptions))
90
119
  * @param {object} [options.hostOptions] options for the default host
91
120
  * @param {(operation: string, params: any) => Promise<any>} [options.dispatchHost] dispatch override
@@ -96,7 +125,7 @@ function toBytes(source, label) {
96
125
  * createBrowserModuleHarness (surface, args, env, allowRawInvoke, logOutput, ...)
97
126
  */
98
127
  export async function createWorkerModuleHarness(options = {}) {
99
- const wasmBytes = toBytes(options.wasmSource, "wasmSource");
128
+ const wasmModule = await toWasmModule(options.wasmSource, "wasmSource");
100
129
  const host = options.host ?? createBrowserHost(options.hostOptions);
101
130
  const dispatch = options.dispatchHost ?? createAsyncHostDispatcher(host);
102
131
  const buffer = createSabHostcallBuffer({
@@ -150,7 +179,7 @@ export async function createWorkerModuleHarness(options = {}) {
150
179
  case "worker-online":
151
180
  port.post({
152
181
  type: "init",
153
- wasmBytes,
182
+ wasmModule,
154
183
  buffer,
155
184
  hostcallTimeoutMs: options.hostcallTimeoutMs,
156
185
  harnessOptions: {
@@ -90,12 +90,13 @@ async function handleInit(message) {
90
90
  timeoutMs: message.hostcallTimeoutMs,
91
91
  postRequest: (request) => port.post({ type: "hostcall", ...request }),
92
92
  });
93
+ // Plaintext-hygiene contract: the controlling thread compiles once and
94
+ // ships the WebAssembly.Module (never raw bytes) — see toWasmModule() in
95
+ // workerModuleHarness.js. createBrowserModuleHarness's compileWasmModule
96
+ // already returns a Module input unchanged, so this never re-compiles.
93
97
  const harnessOptions = {
94
98
  ...(message.harnessOptions ?? {}),
95
- wasmSource:
96
- message.wasmBytes instanceof Uint8Array
97
- ? message.wasmBytes
98
- : new Uint8Array(message.wasmBytes),
99
+ wasmSource: message.wasmModule,
99
100
  hostcallDispatch: dispatch,
100
101
  };
101
102
  harness = await createBrowserModuleHarness(harnessOptions);
@@ -80,6 +80,45 @@ async function deriveAesKey(sharedSecret, salt, context) {
80
80
  );
81
81
  }
82
82
 
83
+ // SAW-M3: the recipient's raw private scalar never has to enter this module.
84
+ // A caller (e.g. a browser session backed by hd-wallet-wasm) may supply a
85
+ // `keyAgreement` provider that performs step 1 (the X25519 ECDH, the ONLY
86
+ // step that needs the scalar) behind its own custody boundary and resolves
87
+ // the 32-byte shared secret. Steps 2-3 (HKDF derive, AES-GCM) stay here —
88
+ // public math, curve-agnostic, not duplicated into the wallet.
89
+ function keyExchangeFromAlgorithm(algorithm) {
90
+ if (typeof algorithm !== "string" || !algorithm.length) {
91
+ return "X25519";
92
+ }
93
+ return algorithm.split("-")[0] || "X25519";
94
+ }
95
+
96
+ async function resolveSharedSecret(
97
+ label,
98
+ { recipientPrivateKey, keyAgreement, ephemeralPublicKey, context, keyExchange },
99
+ ) {
100
+ if (recipientPrivateKey && keyAgreement) {
101
+ throw new Error(
102
+ `${label} accepts either recipientPrivateKey or keyAgreement, not both.`,
103
+ );
104
+ }
105
+ if (keyAgreement) {
106
+ const sharedSecret = toUint8Array(
107
+ await keyAgreement({ ephemeralPublicKey, context, keyExchange }),
108
+ );
109
+ if (sharedSecret.length !== 32) {
110
+ throw new Error(
111
+ `${label} keyAgreement provider must resolve a 32-byte shared secret.`,
112
+ );
113
+ }
114
+ return sharedSecret;
115
+ }
116
+ if (!recipientPrivateKey) {
117
+ throw new Error(`${label} requires recipientPrivateKey or keyAgreement.`);
118
+ }
119
+ return deriveSharedSecret(recipientPrivateKey, ephemeralPublicKey);
120
+ }
121
+
83
122
  function normalizeOptionalBytes(value) {
84
123
  if (value === null || value === undefined) {
85
124
  return new Uint8Array(0);
@@ -308,15 +347,22 @@ export async function protectMarketplaceContent({
308
347
  export async function decryptMarketplaceContentKeyWrap({
309
348
  wrap,
310
349
  recipientPrivateKey,
350
+ keyAgreement,
311
351
  } = {}) {
312
352
  if (!wrap) {
313
353
  throw new Error("decryptMarketplaceContentKeyWrap requires wrap.");
314
354
  }
315
- const sharedSecret = await deriveSharedSecret(
316
- recipientPrivateKey,
317
- wrap.providerEphemeralPublicKey,
318
- );
319
355
  const wrapContext = `marketplace-content-key:${wrap.providerId}:${wrap.contentKeyId}:${wrap.recipientKeyId}`;
356
+ const sharedSecret = await resolveSharedSecret(
357
+ "decryptMarketplaceContentKeyWrap",
358
+ {
359
+ recipientPrivateKey,
360
+ keyAgreement,
361
+ ephemeralPublicKey: wrap.providerEphemeralPublicKey,
362
+ context: wrapContext,
363
+ keyExchange: keyExchangeFromAlgorithm(wrap.algorithm),
364
+ },
365
+ );
320
366
  const wrapKey = await deriveAesKey(sharedSecret, new Uint8Array(0), wrapContext);
321
367
  const keyMaterial = await aesGcmDecrypt(
322
368
  wrapKey,
@@ -364,16 +410,20 @@ async function encryptBytesLegacy({
364
410
  export async function decryptProtectedBytes({
365
411
  protectedBytes,
366
412
  recipientPrivateKey,
413
+ keyAgreement,
367
414
  } = {}) {
368
415
  const parsed = extractPublicationRecordCollection(protectedBytes);
369
416
  if (!parsed?.enc) {
370
417
  return toUint8Array(protectedBytes);
371
418
  }
372
419
  assertGcmEncRecord(parsed.enc);
373
- const sharedSecret = await deriveSharedSecret(
420
+ const sharedSecret = await resolveSharedSecret("decryptProtectedBytes", {
374
421
  recipientPrivateKey,
375
- parsed.enc.ephemeralPublicKey,
376
- );
422
+ keyAgreement,
423
+ ephemeralPublicKey: parsed.enc.ephemeralPublicKey,
424
+ context: parsed.enc.context ?? "",
425
+ keyExchange: parsed.enc.keyExchange,
426
+ });
377
427
  const aesKey = await deriveAesKey(
378
428
  sharedSecret,
379
429
  new Uint8Array(0),
@@ -0,0 +1,227 @@
1
+ /*
2
+ * Provider Access ABI — guest header.
3
+ *
4
+ * One generalized port for imagery/terrain providers. Identical import names
5
+ * and signatures in the browser, in native WasmEdge and in Docker WasmEdge.
6
+ *
7
+ * Build: clang --target=wasm32-wasip1-threads (never emcc -pthread).
8
+ * The module is EH-free and there is exactly one dist/isomorphic/module.wasm.
9
+ *
10
+ * NOTE ON TYPES: every parameter and every result below is int32_t. No int64_t
11
+ * appears anywhere in a boundary signature. This is deliberate — a 64-bit
12
+ * import parameter legalizes differently depending on how the host
13
+ * instantiates the module, and the mismatch shows up as a link failure or a
14
+ * silently truncated argument in exactly one runtime. Every 64-bit quantity in
15
+ * this ABI travels inside the descriptor struct below, in guest memory, as
16
+ * plain little-endian IEEE-754 that you read with a normal load.
17
+ *
18
+ * See docs/provider-access-abi.md for the normative contract.
19
+ */
20
+
21
+ #ifndef SPACE_DATA_PROVIDER_ABI_H
22
+ #define SPACE_DATA_PROVIDER_ABI_H
23
+
24
+ #include <stdint.h>
25
+
26
+ #ifdef __cplusplus
27
+ extern "C" {
28
+ #endif
29
+
30
+ #define SDM_PROVIDER_IMPORT_MODULE "space_data_provider"
31
+ #define SDM_PROVIDER_ABI_VERSION 1
32
+ #define SDM_PROVIDER_TILE_DESC_MAGIC 0x53445054u /* 'SDPT' */
33
+ #define SDM_PROVIDER_TILE_DESC_BYTES 128
34
+
35
+ /* ---- kinds ------------------------------------------------------------- */
36
+ #define SDM_PROVIDER_KIND_TERRAIN 1u
37
+ #define SDM_PROVIDER_KIND_IMAGERY 2u
38
+
39
+ /* ---- encodings --------------------------------------------------------- */
40
+ #define SDM_PROVIDER_ENC_HEIGHT_F32 1u /* float, metres above the ellipsoid */
41
+ #define SDM_PROVIDER_ENC_HEIGHT_F64 2u /* double, metres above the ellipsoid */
42
+ #define SDM_PROVIDER_ENC_RGBA8 16u
43
+ #define SDM_PROVIDER_ENC_RGB8 17u
44
+ #define SDM_PROVIDER_ENC_GRAY8 18u
45
+ #define SDM_PROVIDER_ENC_GRAY16 19u
46
+ #define SDM_PROVIDER_ENC_RGBA_F32 20u
47
+
48
+ /* ---- flags ------------------------------------------------------------- */
49
+ #define SDM_PROVIDER_FLAG_INTERPOLATED (1u << 0)
50
+ #define SDM_PROVIDER_FLAG_STAGED (1u << 1)
51
+ #define SDM_PROVIDER_FLAG_PARTIAL (1u << 2)
52
+ #define SDM_PROVIDER_FLAG_DERIVED (1u << 3)
53
+ #define SDM_PROVIDER_FLAG_FIXTURE (1u << 4)
54
+
55
+ /* ---- cost classes ------------------------------------------------------ *
56
+ * "maxCost" in the acquire request defaults to DEQUANTIZE. That default IS
57
+ * the "never re-fetch, never re-parse" rule, enforced by the ABI rather than
58
+ * by discipline. Raising it is how a caller says out loud that it accepts a
59
+ * cost.
60
+ */
61
+ #define SDM_PROVIDER_COST_RESIDENT 0u
62
+ #define SDM_PROVIDER_COST_DEQUANTIZE 1u
63
+ #define SDM_PROVIDER_COST_REDECODE 2u
64
+ #define SDM_PROVIDER_COST_REFETCH 3u
65
+ #define SDM_PROVIDER_COST_READBACK 4u
66
+
67
+ /* ---- level-selection provenance ---------------------------------------- *
68
+ * A consumer that cannot see which level answered cannot tell a solve that
69
+ * resolved the ridges from one that interpolated them away. Names mirror the
70
+ * engine consumer's existing vocabulary.
71
+ */
72
+ #define SDM_PROVIDER_STRATEGY_DEFAULT 0u
73
+ #define SDM_PROVIDER_STRATEGY_GRID_MATCHED_LEVEL 1u
74
+ #define SDM_PROVIDER_STRATEGY_MOST_DETAILED 2u
75
+ #define SDM_PROVIDER_STRATEGY_FIXED_LEVEL 3u
76
+
77
+ /* ---- error codes ------------------------------------------------------- *
78
+ * Negative results from acquire/read/release. Identical value, identical
79
+ * meaning and identical trap class (none — these are VALUES) in every runtime.
80
+ */
81
+ #define SDM_PROVIDER_E_INVALID_REQUEST (-1)
82
+ #define SDM_PROVIDER_E_NO_CAPABILITY (-2)
83
+ #define SDM_PROVIDER_E_NO_PROVIDER (-3)
84
+ #define SDM_PROVIDER_E_NOT_READY (-4)
85
+ #define SDM_PROVIDER_E_NOT_AVAILABLE (-5)
86
+ #define SDM_PROVIDER_E_BOUNDS (-6)
87
+ #define SDM_PROVIDER_E_BAD_HANDLE (-7)
88
+ #define SDM_PROVIDER_E_BAD_PLANE (-8)
89
+ #define SDM_PROVIDER_E_UNSUPPORTED (-9)
90
+ #define SDM_PROVIDER_E_TIMEOUT (-10)
91
+ #define SDM_PROVIDER_E_HOST (-11)
92
+ #define SDM_PROVIDER_E_PORT_UNAVAILABLE (-12)
93
+
94
+ /* ---- no-data sentinel -------------------------------------------------- *
95
+ * NOT NaN. WebAssembly does not canonicalize NaN payloads across every
96
+ * producing operation, so two runtimes can hold different bits for "a NaN" and
97
+ * a byte-identical-output assertion would fail on semantically equal values.
98
+ * These have exactly one encoding each and are never a real terrain height.
99
+ */
100
+ #define SDM_PROVIDER_NO_DATA_F32_BITS 0xFF7FFFFFu
101
+ #define SDM_PROVIDER_NO_DATA_F64_BITS 0xFFEFFFFFFFFFFFFFull
102
+
103
+ static inline int sdm_provider_is_no_data_f64(double value) {
104
+ uint64_t bits;
105
+ __builtin_memcpy(&bits, &value, sizeof bits);
106
+ return bits == SDM_PROVIDER_NO_DATA_F64_BITS;
107
+ }
108
+
109
+ static inline int sdm_provider_is_no_data_f32(float value) {
110
+ uint32_t bits;
111
+ __builtin_memcpy(&bits, &value, sizeof bits);
112
+ return bits == SDM_PROVIDER_NO_DATA_F32_BITS;
113
+ }
114
+
115
+ /* ---- tile descriptor --------------------------------------------------- *
116
+ * 128 bytes, little-endian. All f64 fields are 8-byte aligned, so this struct
117
+ * is read with ordinary aligned loads on every target.
118
+ */
119
+ typedef struct sdm_provider_tile_desc {
120
+ uint32_t magic; /* 0 SDM_PROVIDER_TILE_DESC_MAGIC */
121
+ uint32_t version; /* 4 SDM_PROVIDER_ABI_VERSION */
122
+ uint32_t kind; /* 8 */
123
+ uint32_t encoding; /* 12 */
124
+ uint32_t width; /* 16 elements per row */
125
+ uint32_t height; /* 20 rows */
126
+ uint32_t plane_count; /* 24 */
127
+ uint32_t bytes_per_element; /* 28 */
128
+ uint32_t row_stride_bytes; /* 32 */
129
+ uint32_t byte_length; /* 36 plane 0 */
130
+ uint32_t flags; /* 40 */
131
+ uint32_t level; /* 44 0xFFFFFFFF for derived tiles */
132
+ double west; /* 48 radians */
133
+ double south; /* 56 */
134
+ double east; /* 64 */
135
+ double north; /* 72 */
136
+ double min_value; /* 80 terrain: min height, metres */
137
+ double max_value; /* 88 terrain: max height, metres */
138
+ uint32_t tile_x; /* 96 */
139
+ uint32_t tile_y; /* 100 */
140
+ uint32_t host_copies; /* 104 host->guest copies per whole-plane read */
141
+ uint32_t source_id; /* 108 FNV-1a 32 of the provider id */
142
+ uint32_t cost_class; /* 112 what this acquire actually cost */
143
+ uint32_t strategy; /* 116 how the level was chosen */
144
+ uint32_t reserved[2]; /* 120 */
145
+ } sdm_provider_tile_desc_t;
146
+
147
+ _Static_assert(sizeof(sdm_provider_tile_desc_t) == SDM_PROVIDER_TILE_DESC_BYTES,
148
+ "sdm_provider_tile_desc_t must be exactly 128 bytes");
149
+
150
+ /* ---- imports ----------------------------------------------------------- *
151
+ * Open, read, close. Three doors.
152
+ */
153
+
154
+ /*
155
+ * Acquire pins a provider's decoded buffer host-side and fills `desc`.
156
+ * `request` is UTF-8 JSON:
157
+ *
158
+ * {"op":"tile","providerId":"...","level":9,"x":1,"y":2}
159
+ * {"op":"profile","providerId":"...","start":[lon,lat],"end":[lon,lat],
160
+ * "samples":256,"level":"mostDetailed"}
161
+ * {"op":"profile","providerId":"...","positionsPtr":P,"positionsCount":N}
162
+ * {"op":"region","providerId":"...","rectangle":[w,s,e,n],
163
+ * "width":256,"height":256,"level":9}
164
+ *
165
+ * plus optional "maxCost" (default 1), "plane", and "spacing" (the target
166
+ * sample spacing in METRES — preferred over "level": you know your own march
167
+ * stride, you do not know a provider's level scheme).
168
+ *
169
+ * RASTER IN: a coverage field is commonly 512x512 = 262,144 positions.
170
+ * Encoding those as JSON would be a multi-megabyte request string parsed on
171
+ * every call — a worse cost than the read it is asking for. Pass
172
+ * "positionsPtr"/"positionsCount" instead and the host reads interleaved f64
173
+ * lon/lat pairs straight out of guest memory with ONE copy, symmetric with the
174
+ * way results come back.
175
+ *
176
+ * Returns a handle > 0, or a negative SDM_PROVIDER_E_* code. `desc` is left
177
+ * untouched on failure. The call blocks until the tile is resident.
178
+ */
179
+ __attribute__((import_module(SDM_PROVIDER_IMPORT_MODULE),
180
+ import_name("acquire"))) extern int32_t
181
+ sdm_provider_acquire_raw(int32_t request_ptr, int32_t request_len,
182
+ int32_t desc_ptr);
183
+
184
+ /*
185
+ * Copies at most `dst_len` bytes of `plane`, starting `src_offset` bytes into
186
+ * that plane, into guest memory at `dst_ptr`. Returns bytes written (which may
187
+ * legally be less than dst_len at the tail), or a negative error code.
188
+ *
189
+ * Reading a plane in several chunks yields exactly the same bytes as one
190
+ * whole-plane read.
191
+ */
192
+ __attribute__((import_module(SDM_PROVIDER_IMPORT_MODULE),
193
+ import_name("read"))) extern int32_t
194
+ sdm_provider_read_raw(int32_t handle, int32_t plane, int32_t src_offset,
195
+ int32_t dst_ptr, int32_t dst_len);
196
+
197
+ /* Unpins. Releasing an already-released handle returns E_BAD_HANDLE. */
198
+ __attribute__((import_module(SDM_PROVIDER_IMPORT_MODULE),
199
+ import_name("release"))) extern int32_t
200
+ sdm_provider_release(int32_t handle);
201
+
202
+ /* ---- pointer-friendly wrappers ---------------------------------------- */
203
+
204
+ static inline int32_t sdm_provider_acquire(const char *request,
205
+ int32_t request_len,
206
+ sdm_provider_tile_desc_t *desc) {
207
+ return sdm_provider_acquire_raw((int32_t)(uintptr_t)request, request_len,
208
+ (int32_t)(uintptr_t)desc);
209
+ }
210
+
211
+ static inline int32_t sdm_provider_read(int32_t handle, int32_t plane,
212
+ int32_t src_offset, void *dst,
213
+ int32_t dst_len) {
214
+ return sdm_provider_read_raw(handle, plane, src_offset,
215
+ (int32_t)(uintptr_t)dst, dst_len);
216
+ }
217
+
218
+ static inline int sdm_provider_desc_valid(const sdm_provider_tile_desc_t *desc) {
219
+ return desc != 0 && desc->magic == SDM_PROVIDER_TILE_DESC_MAGIC &&
220
+ desc->version == SDM_PROVIDER_ABI_VERSION;
221
+ }
222
+
223
+ #ifdef __cplusplus
224
+ } /* extern "C" */
225
+ #endif
226
+
227
+ #endif /* SPACE_DATA_PROVIDER_ABI_H */