react-native-cesium 1.2.2 → 1.2.4

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 (40) hide show
  1. package/README.md +100 -26
  2. package/android/CMakeLists.txt +1 -2
  3. package/android/src/main/cpp/CesiumBridgeAndroid.cpp +264 -130
  4. package/android/src/main/cpp/CesiumBridgeAndroid.h +70 -27
  5. package/android/src/main/java/com/margelo/nitro/reactnativecesium/CesiumBridgeJNI.kt +68 -44
  6. package/android/src/main/java/com/margelo/nitro/reactnativecesium/HybridCesiumView.kt +160 -187
  7. package/cpp/engine/CameraIntegrator.cpp +374 -0
  8. package/cpp/engine/CameraIntegrator.hpp +146 -0
  9. package/cpp/engine/CesiumEngine.cpp +3 -1
  10. package/cpp/engine/EngineTunables.hpp +80 -21
  11. package/cpp/engine/GlobeCamera.cpp +17 -6
  12. package/cpp/engine/GlobeCamera.hpp +11 -8
  13. package/cpp/engine/test/CameraIntegratorTest.cpp +205 -0
  14. package/cpp/engine/test/build_and_run.sh +27 -0
  15. package/example/README.md +1 -1
  16. package/ios/CesiumBridge.h +53 -28
  17. package/ios/CesiumBridge.mm +172 -86
  18. package/ios/HybridCesiumView.swift +213 -145
  19. package/lib/typescript/src/specs/cesium-view.nitro.d.ts +77 -13
  20. package/lib/typescript/src/specs/cesium-view.nitro.d.ts.map +1 -1
  21. package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.cpp +90 -9
  22. package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.hpp +19 -3
  23. package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.cpp +20 -0
  24. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/HybridCesiumViewSpec.kt +57 -3
  25. package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Bridge.hpp +9 -9
  26. package/nitrogen/generated/ios/c++/HybridCesiumViewSpecSwift.hpp +82 -9
  27. package/nitrogen/generated/ios/c++/views/HybridCesiumViewComponent.mm +25 -0
  28. package/nitrogen/generated/ios/swift/HybridCesiumViewSpec.swift +14 -3
  29. package/nitrogen/generated/ios/swift/HybridCesiumViewSpec_cxx.swift +210 -16
  30. package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.cpp +19 -3
  31. package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.hpp +20 -4
  32. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.cpp +55 -0
  33. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.hpp +5 -0
  34. package/nitrogen/generated/shared/json/CesiumViewConfig.json +5 -0
  35. package/package.json +2 -2
  36. package/src/specs/cesium-view.nitro.ts +93 -14
  37. package/cpp/engine/CameraSmoother.cpp +0 -71
  38. package/cpp/engine/CameraSmoother.hpp +0 -25
  39. package/cpp/engine/CameraTargetState.cpp +0 -140
  40. package/cpp/engine/CameraTargetState.hpp +0 -103
@@ -2,8 +2,18 @@ import Foundation
2
2
  import Metal
3
3
  import MetalKit
4
4
  import NitroModules
5
+ import QuartzCore
5
6
  import UIKit
6
7
 
8
+ /// Weak proxy for CADisplayLink so the timer does not strongly retain its
9
+ /// owning view. Without this, CADisplayLink → HybridCesiumView → displayLink
10
+ /// forms a cycle that prevents `deinit` from ever firing, which in turn
11
+ /// prevents the C++ engine and SqliteCache handle from being released.
12
+ private final class DisplayLinkProxy {
13
+ weak var owner: HybridCesiumView?
14
+ @objc func tick() { owner?.handleDisplayLinkTick() }
15
+ }
16
+
7
17
  class HybridCesiumView: HybridCesiumViewSpec {
8
18
  // MARK: - Props
9
19
 
@@ -22,9 +32,9 @@ class HybridCesiumView: HybridCesiumViewSpec {
22
32
  }
23
33
  }
24
34
 
25
- /// Construction-time seed camera. Used exactly once when the native bridge
26
- /// is created; subsequent prop writes are ignored. Use `setCamera(_:)` to
27
- /// drive the camera at runtime — it does NOT mutate this value.
35
+ /// Construction-time seed camera. Used exactly once via `teleport(...)`
36
+ /// when the native bridge is created; subsequent prop writes are ignored.
37
+ /// Use the per-DoF setters (or `teleport(...)`) for runtime updates.
28
38
  var initialCamera: CameraState = CameraState(
29
39
  latitude: 46.15,
30
40
  longitude: 7.35,
@@ -55,7 +65,6 @@ class HybridCesiumView: HybridCesiumViewSpec {
55
65
  var msaaSampleCount: Double = 1 {
56
66
  didSet {
57
67
  let s = Int32(msaaSampleCount)
58
- // applyMtkViewMsaa touches MTKView/UIKit so it stays on main.
59
68
  applyMtkViewMsaa(Int(s))
60
69
  postToRender { [weak self] in self?.bridge?.setMsaaSampleCount(s) }
61
70
  }
@@ -105,12 +114,31 @@ class HybridCesiumView: HybridCesiumViewSpec {
105
114
  didSet { if let v = minAltitudeAboveTerrain { postToRender { [weak self] in self?.bridge?.setMinAltitudeAboveTerrain(Float(v)) } } }
106
115
  }
107
116
 
117
+ // ── Rate caps ────────────────────────────────────────────────────────────
118
+ var maxYawRateDegSec: Double? { didSet { pushRateCaps() } }
119
+ var maxPitchRateDegSec: Double? { didSet { pushRateCaps() } }
120
+ var maxRollRateDegSec: Double? { didSet { pushRateCaps() } }
121
+ var maxClimbRateMps: Double? { didSet { pushRateCaps() } }
122
+ var maxGroundSpeedMps: Double? { didSet { pushRateCaps() } }
123
+
124
+ private func pushRateCaps() {
125
+ let yaw = maxYawRateDegSec ?? 0
126
+ let pitch = maxPitchRateDegSec ?? 0
127
+ let roll = maxRollRateDegSec ?? 0
128
+ let climb = maxClimbRateMps ?? 0
129
+ let gnd = maxGroundSpeedMps ?? 0
130
+ postToRender { [weak self] in
131
+ self?.bridge?.setRateCapsYaw(yaw, pitch: pitch, roll: roll,
132
+ climb: climb, groundSpeed: gnd)
133
+ }
134
+ }
135
+
108
136
  // MARK: - Render-thread dispatch helper
109
137
 
110
138
  /// Posts a closure to the dedicated render queue. Used by every prop setter
111
- /// that mutates engine state, so the EngineConfig + GPU resources are only
112
- /// ever touched from one thread (renderQueue) — the render path itself, the
113
- /// surface init, and bridge teardown all run there.
139
+ /// that mutates engine state. Camera demand setters do **not** hop here —
140
+ /// the CameraIntegrator's mutex makes per-DoF writes safe from any thread,
141
+ /// and skipping the dispatch keeps latency minimal for high-rate updates.
114
142
  private func postToRender(_ action: @escaping () -> Void) {
115
143
  renderQueue.async { action() }
116
144
  }
@@ -119,7 +147,30 @@ class HybridCesiumView: HybridCesiumViewSpec {
119
147
 
120
148
  // MARK: - Methods
121
149
 
122
- func getCameraState() throws -> Promise<CameraState> {
150
+ func getActualCamera() throws -> Promise<CameraState> {
151
+ guard let b = bridge else {
152
+ return Promise.rejected(
153
+ withError: NSError(
154
+ domain: "HybridCesiumView",
155
+ code: 1,
156
+ userInfo: [NSLocalizedDescriptionKey: "Native bridge not initialized"]
157
+ )
158
+ )
159
+ }
160
+ return Promise.resolved(
161
+ withResult: CameraState(
162
+ latitude: b.readActualLatitude(),
163
+ longitude: b.readActualLongitude(),
164
+ altitude: b.readActualAltitude(),
165
+ heading: b.readActualHeading(),
166
+ pitch: b.readActualPitch(),
167
+ roll: b.readActualRoll(),
168
+ verticalFovDeg: b.readActualVerticalFovDeg()
169
+ )
170
+ )
171
+ }
172
+
173
+ func getDemandCamera() throws -> Promise<CameraState> {
123
174
  guard let b = bridge else {
124
175
  return Promise.rejected(
125
176
  withError: NSError(
@@ -129,47 +180,79 @@ class HybridCesiumView: HybridCesiumViewSpec {
129
180
  )
130
181
  )
131
182
  }
132
- let s = CameraState(
133
- latitude: b.readCameraLatitude(),
134
- longitude: b.readCameraLongitude(),
135
- altitude: b.readCameraAltitude(),
136
- heading: b.readCameraHeading(),
137
- pitch: b.readCameraPitch(),
138
- roll: b.readCameraRoll(),
139
- verticalFovDeg: b.readVerticalFovDeg()
183
+ return Promise.resolved(
184
+ withResult: CameraState(
185
+ latitude: b.readDemandLatitude(),
186
+ longitude: b.readDemandLongitude(),
187
+ altitude: b.readDemandAltitude(),
188
+ heading: b.readDemandHeading(),
189
+ pitch: b.readDemandPitch(),
190
+ roll: b.readDemandRoll(),
191
+ verticalFovDeg: b.readDemandVerticalFovDeg()
192
+ )
140
193
  )
141
- return Promise.resolved(withResult: s)
142
194
  }
143
195
 
144
- func setCamera(camera: CameraState) throws {
145
- // Runtime camera updates flow through this method. We do *not* mutate
146
- // `initialCamera` (it is construction-time only). If the view is not
147
- // ready yet, the value is buffered in `pendingRuntimeCamera` and pushed
148
- // during initialization.
149
- pendingRuntimeCamera = camera
150
- pendingRuntimeViewCorrection = nil
151
- postToRender { [weak self] in self?.pushCameraStateIfChanged(camera) }
196
+ func setPosition(latitude: Double, longitude: Double) throws {
197
+ if let b = bridge {
198
+ b.setPositionLatitude(latitude, longitude: longitude)
199
+ } else {
200
+ pendingLat = latitude
201
+ pendingLon = longitude
202
+ }
203
+ }
204
+
205
+ func setAltitude(altitudeMeters: Double) throws {
206
+ if let b = bridge { b.setAltitude(altitudeMeters) }
207
+ else { pendingAltitude = altitudeMeters }
208
+ }
209
+
210
+ func setHeading(headingDeg: Double) throws {
211
+ if let b = bridge { b.setHeadingDeg(headingDeg) }
212
+ else { pendingHeading = headingDeg }
213
+ }
214
+
215
+ func setAttitude(pitchDeg: Double, rollDeg: Double) throws {
216
+ if let b = bridge { b.setAttitudePitch(pitchDeg, roll: rollDeg) }
217
+ else { pendingPitch = pitchDeg; pendingRoll = rollDeg }
218
+ }
219
+
220
+ func setViewCorrection(q: Quaternion) throws {
221
+ if let b = bridge { b.setViewCorrectionW(q.w, x: q.x, y: q.y, z: q.z) }
222
+ else { pendingViewCorrection = q }
152
223
  }
153
224
 
154
- func setCameraQuaternion(camera: CameraState, viewCorrection: Quaternion) throws {
155
- pendingRuntimeCamera = camera
156
- pendingRuntimeViewCorrection = viewCorrection
157
- postToRender { [weak self] in self?.pushCameraQuaternionIfChanged(camera, viewCorrection: viewCorrection) }
225
+ func setVerticalFov(deg: Double) throws {
226
+ if let b = bridge { b.setVerticalFovDeg(deg) }
227
+ else { pendingVfov = deg }
228
+ }
229
+
230
+ func teleport(camera: CameraState) throws {
231
+ if let b = bridge {
232
+ b.teleportLatitude(camera.latitude,
233
+ longitude: camera.longitude,
234
+ altitude: camera.altitude,
235
+ heading: camera.heading,
236
+ pitch: camera.pitch,
237
+ roll: camera.roll,
238
+ verticalFovDeg: camera.verticalFovDeg)
239
+ } else {
240
+ pendingTeleport = camera
241
+ }
158
242
  }
159
243
 
160
244
  func getViewCorrection() throws -> Promise<Quaternion> {
161
245
  guard let b = bridge else {
162
- return Promise.resolved(
163
- withResult: Quaternion(w: 1, x: 0, y: 0, z: 0)
164
- )
246
+ return Promise.resolved(withResult: Quaternion(w: 1, x: 0, y: 0, z: 0))
165
247
  }
166
- let q = Quaternion(
167
- w: b.readViewCorrectionW(),
168
- x: b.readViewCorrectionX(),
169
- y: b.readViewCorrectionY(),
170
- z: b.readViewCorrectionZ()
248
+ return Promise.resolved(
249
+ withResult: Quaternion(
250
+ w: b.readViewCorrectionW(),
251
+ x: b.readViewCorrectionX(),
252
+ y: b.readViewCorrectionY(),
253
+ z: b.readViewCorrectionZ()
254
+ )
171
255
  )
172
- return Promise.resolved(withResult: q)
173
256
  }
174
257
 
175
258
  // MARK: - View
@@ -177,36 +260,50 @@ class HybridCesiumView: HybridCesiumViewSpec {
177
260
  private let metalView: MTKView
178
261
  private var bridge: CesiumBridge?
179
262
  private var displayLink: CADisplayLink?
263
+ private let displayLinkProxy = DisplayLinkProxy()
180
264
  private var layoutPollTimer: Timer?
181
265
  private var metricsFrameCounter: Int = 0
182
266
  private var idleProbeAccumulator: Double = 0
183
267
  private var usingLowRefreshRate = false
184
268
  private var hasConfiguredFrameRate = false
185
- private var lastPushedCameraState: CameraState?
186
- /// Last quaternion pushed via `setCameraQuaternion`; `nil` if only `setCamera` has been used.
187
- private var lastPushedViewCorrection: Quaternion?
188
- /// Camera state requested by `setCamera` before the bridge was ready.
189
- private var pendingRuntimeCamera: CameraState?
190
- private var pendingRuntimeViewCorrection: Quaternion?
191
269
  private var lastBridgePixelSize: CGSize = .zero
192
270
 
271
+ /// Demand values that arrived before the bridge was created. Drained once
272
+ /// in `ensureInitialized()` after the seed teleport.
273
+ private var pendingTeleport: CameraState?
274
+ private var pendingLat: Double?
275
+ private var pendingLon: Double?
276
+ private var pendingAltitude: Double?
277
+ private var pendingHeading: Double?
278
+ private var pendingPitch: Double?
279
+ private var pendingRoll: Double?
280
+ private var pendingVfov: Double?
281
+ private var pendingViewCorrection: Quaternion?
282
+
193
283
  /// Dedicated serial render queue. CADisplayLink fires on `.main`, but the
194
284
  /// actual `bridge.shouldRenderNextFrame()` / `bridge.renderFrame(...)` work
195
285
  /// is dispatched here so the main thread is never blocked on Cesium tile
196
286
  /// loading or Metal command-buffer encoding. All bridge prop setters also
197
287
  /// dispatch here so they serialize with rendering and never race with
198
- /// engine state.
288
+ /// engine state. Per-DoF camera writes deliberately bypass this hop —
289
+ /// CameraIntegrator's internal mutex handles cross-thread safety.
199
290
  private let renderQueue = DispatchQueue(
200
291
  label: "com.margelo.cesium.render",
201
292
  qos: .userInteractive
202
293
  )
203
294
  /// Drops a CADisplayLink tick if the render queue is still finishing the
204
- /// previous frame — keeps it from queuing up an unbounded backlog under
205
- /// stutter (Metal's frameSemaphore would already throttle, but this saves
206
- /// the dispatch hop and keeps `dt` accurate).
295
+ /// previous frame.
207
296
  private var renderInFlight = false
208
297
  private let renderInFlightLock = NSLock()
209
298
 
299
+ /// Set to true at the start of `deinit`. Any work still queued on the
300
+ /// render or main queues checks this before touching the bridge or
301
+ /// invoking JS callbacks. Avoids two classes of warnings during fast
302
+ /// refresh / unmount: Nitro "Dispatcher has already been destroyed"
303
+ /// when emitting `onMetrics` into a dying JS runtime, and stray frame
304
+ /// ticks running after the bridge has been torn down.
305
+ private var isShutdown = false
306
+
210
307
  var view: UIView { metalView }
211
308
 
212
309
  override init() {
@@ -270,17 +367,29 @@ class HybridCesiumView: HybridCesiumViewSpec {
270
367
  if !ionAccessToken.isEmpty {
271
368
  bridge?.updateIonAccessToken(ionAccessToken, assetId: Int64(ionAssetId))
272
369
  }
273
- // Apply construction-time seed first; if the consumer already invoked
274
- // setCamera / setCameraQuaternion before we were ready, replay it last so
275
- // the runtime value wins.
276
- pushCameraStateIfChanged(initialCamera)
277
- if let runtime = pendingRuntimeCamera {
278
- if let q = pendingRuntimeViewCorrection {
279
- pushCameraQuaternionIfChanged(runtime, viewCorrection: q)
280
- } else {
281
- pushCameraStateIfChanged(runtime)
282
- }
370
+ // Seed from initialCamera, then replay anything the consumer set before
371
+ // we were ready.
372
+ let seed = pendingTeleport ?? initialCamera
373
+ bridge?.teleportLatitude(seed.latitude,
374
+ longitude: seed.longitude,
375
+ altitude: seed.altitude,
376
+ heading: seed.heading,
377
+ pitch: seed.pitch,
378
+ roll: seed.roll,
379
+ verticalFovDeg: seed.verticalFovDeg)
380
+ pendingTeleport = nil
381
+
382
+ if let la = pendingLat, let lo = pendingLon {
383
+ bridge?.setPositionLatitude(la, longitude: lo); pendingLat = nil; pendingLon = nil
283
384
  }
385
+ if let a = pendingAltitude { bridge?.setAltitude(a); pendingAltitude = nil }
386
+ if let h = pendingHeading { bridge?.setHeadingDeg(h); pendingHeading = nil }
387
+ if let p = pendingPitch, let r = pendingRoll {
388
+ bridge?.setAttitudePitch(p, roll: r); pendingPitch = nil; pendingRoll = nil
389
+ }
390
+ if let f = pendingVfov { bridge?.setVerticalFovDeg(f); pendingVfov = nil }
391
+ if let q = pendingViewCorrection { bridge?.setViewCorrectionW(q.w, x: q.x, y: q.y, z: q.z); pendingViewCorrection = nil }
392
+
284
393
  if ionImageryAssetId != 1 {
285
394
  bridge?.updateImageryAssetId(Int64(ionImageryAssetId))
286
395
  }
@@ -289,6 +398,8 @@ class HybridCesiumView: HybridCesiumViewSpec {
289
398
  bridge?.setMsaaSampleCount(sc)
290
399
  applyMtkViewMsaa(Int(sc))
291
400
 
401
+ pushRateCaps()
402
+
292
403
  startRenderLoop()
293
404
  }
294
405
 
@@ -325,69 +436,14 @@ class HybridCesiumView: HybridCesiumViewSpec {
325
436
  if let v = minAltitudeAboveTerrain { bridge?.setMinAltitudeAboveTerrain(Float(v)) }
326
437
  }
327
438
 
328
- /// Pushes camera state into native bridge only when values changed.
329
- private func pushCameraStateIfChanged(_ camera: CameraState) {
330
- guard bridge != nil else { return }
331
- if let last = lastPushedCameraState,
332
- last.latitude == camera.latitude,
333
- last.longitude == camera.longitude,
334
- last.altitude == camera.altitude,
335
- last.heading == camera.heading,
336
- last.pitch == camera.pitch,
337
- last.roll == camera.roll,
338
- last.verticalFovDeg == camera.verticalFovDeg {
339
- return
340
- }
341
- lastPushedCameraState = camera
342
- bridge?.updateCameraLatitude(
343
- camera.latitude,
344
- longitude: camera.longitude,
345
- altitude: camera.altitude,
346
- heading: camera.heading,
347
- pitch: camera.pitch,
348
- roll: camera.roll
349
- )
350
- bridge?.setVerticalFovDeg(camera.verticalFovDeg)
351
- }
352
-
353
- private func pushCameraQuaternionIfChanged(_ camera: CameraState, viewCorrection: Quaternion) {
354
- guard bridge != nil else { return }
355
- if let lastCam = lastPushedCameraState,
356
- let lastQ = lastPushedViewCorrection,
357
- lastCam.latitude == camera.latitude,
358
- lastCam.longitude == camera.longitude,
359
- lastCam.altitude == camera.altitude,
360
- lastCam.heading == camera.heading,
361
- lastCam.pitch == camera.pitch,
362
- lastCam.roll == camera.roll,
363
- lastCam.verticalFovDeg == camera.verticalFovDeg,
364
- lastQ.w == viewCorrection.w,
365
- lastQ.x == viewCorrection.x,
366
- lastQ.y == viewCorrection.y,
367
- lastQ.z == viewCorrection.z {
368
- return
369
- }
370
- lastPushedCameraState = camera
371
- lastPushedViewCorrection = viewCorrection
372
- bridge?.updateCameraQuaternionLatitude(
373
- camera.latitude,
374
- longitude: camera.longitude,
375
- altitude: camera.altitude,
376
- heading: camera.heading,
377
- pitch: camera.pitch,
378
- roll: camera.roll,
379
- viewCorrectionW: viewCorrection.w,
380
- x: viewCorrection.x,
381
- y: viewCorrection.y,
382
- z: viewCorrection.z
383
- )
384
- bridge?.setVerticalFovDeg(camera.verticalFovDeg)
385
- }
386
-
387
439
  // MARK: - Render Loop
388
440
 
389
441
  private func startRenderLoop() {
390
- displayLink = CADisplayLink(target: self, selector: #selector(renderFrame))
442
+ displayLinkProxy.owner = self
443
+ displayLink = CADisplayLink(
444
+ target: displayLinkProxy,
445
+ selector: #selector(DisplayLinkProxy.tick)
446
+ )
391
447
  setDisplayLinkFrameRate(idle: false)
392
448
  displayLink?.add(to: .main, forMode: .common)
393
449
  displayLink?.isPaused = pauseRendering
@@ -403,13 +459,12 @@ class HybridCesiumView: HybridCesiumViewSpec {
403
459
  : CAFrameRateRange(minimum: 30, maximum: 60, preferred: 60)
404
460
  }
405
461
 
406
- @objc private func renderFrame() {
462
+ /// Invoked by `DisplayLinkProxy` on every CADisplayLink tick (main thread).
463
+ fileprivate func handleDisplayLinkTick() {
407
464
  guard !pauseRendering,
408
- let dl = displayLink,
465
+ !isShutdown,
409
466
  let bridge else { return }
410
467
 
411
- let dt = max(dl.targetTimestamp - dl.timestamp, 1.0 / 120.0)
412
-
413
468
  // metalView.bounds / drawableSize are UIKit, must stay on main.
414
469
  let scale = metalView.contentScaleFactor
415
470
  let w = Int(metalView.bounds.width * scale)
@@ -424,9 +479,7 @@ class HybridCesiumView: HybridCesiumViewSpec {
424
479
  }
425
480
  }
426
481
 
427
- // Throttle: drop the tick if the render queue hasn't finished the previous
428
- // frame yet. This keeps the dispatch backlog at zero and lets the next
429
- // CADisplayLink fire pick up a fresh dt.
482
+ // Drop the tick if the render queue hasn't finished the previous frame yet.
430
483
  renderInFlightLock.lock()
431
484
  if renderInFlight {
432
485
  renderInFlightLock.unlock()
@@ -435,16 +488,24 @@ class HybridCesiumView: HybridCesiumViewSpec {
435
488
  renderInFlight = true
436
489
  renderInFlightLock.unlock()
437
490
 
491
+ // Wall-clock time used for the metrics-only frame-dt EMA inside the
492
+ // bridge. The camera integrator owns its own clock — see
493
+ // CameraIntegrator::step().
494
+ let nowSeconds = CACurrentMediaTime()
495
+
438
496
  let resizedW = w
439
497
  let resizedH = h
440
- let weakSelf = self // captured for main-thread post-frame work
441
498
  renderQueue.async { [weak self, weak bridge] in
442
- guard let self = self, let bridge = bridge else {
443
- weakSelf.renderInFlightLock.lock()
444
- weakSelf.renderInFlight = false
445
- weakSelf.renderInFlightLock.unlock()
446
- return
499
+ guard let self = self else { return }
500
+ // `renderInFlight` must always be cleared so a single dropped tick
501
+ // doesn't permanently wedge the render loop. defer guarantees it
502
+ // regardless of which early-return path we take below.
503
+ defer {
504
+ self.renderInFlightLock.lock()
505
+ self.renderInFlight = false
506
+ self.renderInFlightLock.unlock()
447
507
  }
508
+ guard let bridge = bridge else { return }
448
509
 
449
510
  if didResize {
450
511
  bridge.resize(Int32(resizedW), height: Int32(resizedH))
@@ -455,12 +516,13 @@ class HybridCesiumView: HybridCesiumViewSpec {
455
516
  let nextIsIdle: Bool
456
517
  if shouldRenderNow {
457
518
  self.idleProbeAccumulator = 0
458
- bridge.renderFrame(withDt: dt)
519
+ bridge.renderFrame(at: nowSeconds)
459
520
  nextIsIdle = false
460
521
  } else {
461
- self.idleProbeAccumulator += dt
522
+ // Approximate dt for the idle probe. We don't need precision here.
523
+ let probeDt = 1.0 / 60.0
524
+ self.idleProbeAccumulator += probeDt
462
525
  if self.idleProbeAccumulator >= 0.25 {
463
- // Safety probe: occasionally tick engine state for late tile completions.
464
526
  self.idleProbeAccumulator = 0
465
527
  bridge.markNeedsRender()
466
528
  }
@@ -469,7 +531,7 @@ class HybridCesiumView: HybridCesiumViewSpec {
469
531
 
470
532
  self.metricsFrameCounter += 1
471
533
  let shouldEmitMetrics =
472
- self.metricsFrameCounter >= 20 && self.onMetrics != nil
534
+ self.metricsFrameCounter >= 20 && self.onMetrics != nil && !self.isShutdown
473
535
  let metricsSnapshot: CesiumMetrics? = shouldEmitMetrics ? CesiumMetrics(
474
536
  fps: bridge.metricsFps,
475
537
  tilesRendered: Double(bridge.metricsTilesRendered),
@@ -482,32 +544,38 @@ class HybridCesiumView: HybridCesiumViewSpec {
482
544
  ) : nil
483
545
  if shouldEmitMetrics { self.metricsFrameCounter = 0 }
484
546
 
485
- // Hop back to main to update CADisplayLink rate (UIKit) and emit the
486
- // metrics callback (consumer may touch UI from it).
487
547
  DispatchQueue.main.async { [weak self] in
488
- guard let self = self else { return }
548
+ guard let self = self, !self.isShutdown else { return }
489
549
  self.setDisplayLinkFrameRate(idle: nextIsIdle)
490
550
  if let m = metricsSnapshot, let cb = self.onMetrics {
491
551
  cb(m)
492
552
  }
493
553
  }
494
-
495
- self.renderInFlightLock.lock()
496
- self.renderInFlight = false
497
- self.renderInFlightLock.unlock()
554
+ // renderInFlight is cleared by the defer at the top of this closure.
498
555
  }
499
556
  }
500
557
 
501
558
  deinit {
559
+ // Set the shutdown gate first so any in-flight render-queue tick and
560
+ // any queued main-queue follow-on (metric emission, frame-rate update)
561
+ // bail out before they touch the bridge or invoke onMetrics into a
562
+ // JS dispatcher that may already be gone.
563
+ isShutdown = true
564
+ onMetrics = nil
565
+
502
566
  layoutPollTimer?.invalidate()
567
+ layoutPollTimer = nil
503
568
  displayLink?.invalidate()
569
+ displayLink = nil
570
+ displayLinkProxy.owner = nil
504
571
 
505
- // Drain the render queue so we don't release `bridge` while it's
506
- // mid-encode. `sync` here is bounded by Metal's frameSemaphore wait —
507
- // which itself is already bounded to 1 s in MetalBackend::shutdown().
508
572
  let bridgeRef = bridge
509
573
  bridge = nil
510
574
  if let b = bridgeRef {
575
+ // Drain anything already on the render queue, then ask the bridge
576
+ // to release its engine + Metal backend synchronously. We do this
577
+ // inside the sync hop so the SqliteCache file handle is closed
578
+ // before any subsequent CesiumView mount can open the same cache.
511
579
  renderQueue.sync {
512
580
  b.shutdown()
513
581
  }
@@ -1,5 +1,8 @@
1
1
  import type { HybridView, HybridViewProps, HybridViewMethods } from 'react-native-nitro-modules';
2
- /** Snapshot of the native camera for persistence / sync with React state. */
2
+ /**
3
+ * Snapshot of the native camera for persistence / sync with React state.
4
+ * Returned by `getActualCamera()` and `getDemandCamera()`.
5
+ */
3
6
  export interface CameraState {
4
7
  latitude: number;
5
8
  longitude: number;
@@ -10,8 +13,9 @@ export interface CameraState {
10
13
  verticalFovDeg: number;
11
14
  }
12
15
  /**
13
- * Unit quaternion (w + xi + yj + zk). Used for camera-space view correction.
14
- * Non-unit values are normalized on the native side.
16
+ * Unit quaternion (w + xi + yj + zk). Used for camera-space view correction
17
+ * applied after HPR (boresight / HUD alignment). Non-unit values are
18
+ * normalised on the native side.
15
19
  */
16
20
  export interface Quaternion {
17
21
  w: number;
@@ -34,7 +38,11 @@ export interface CesiumMetrics {
34
38
  export interface CesiumViewProps extends HybridViewProps {
35
39
  ionAccessToken: string;
36
40
  ionAssetId: number;
37
- /** Construction-time seed camera. Use `setCamera()` for runtime updates. */
41
+ /**
42
+ * Initial camera applied via `teleport(...)` once when the native bridge is
43
+ * created. Subsequent prop writes are ignored — use the per-DoF setters or
44
+ * `teleport(...)` for runtime updates.
45
+ */
38
46
  initialCamera: CameraState;
39
47
  pauseRendering: boolean;
40
48
  maximumScreenSpaceError: number;
@@ -74,22 +82,78 @@ export interface CesiumViewProps extends HybridViewProps {
74
82
  * terrain mesh vertex to the camera nadir.
75
83
  */
76
84
  minAltitudeAboveTerrain?: number;
85
+ /** Maximum yaw rate (degrees per second). 0 = uncapped. */
86
+ maxYawRateDegSec?: number;
87
+ /** Maximum pitch rate (degrees per second). 0 = uncapped. */
88
+ maxPitchRateDegSec?: number;
89
+ /** Maximum roll rate (degrees per second). 0 = uncapped. */
90
+ maxRollRateDegSec?: number;
91
+ /** Maximum climb / descent rate (metres per second). 0 = uncapped. */
92
+ maxClimbRateMps?: number;
93
+ /** Maximum horizontal ground speed (metres per second). 0 = uncapped. */
94
+ maxGroundSpeedMps?: number;
77
95
  onMetrics?: (metrics: CesiumMetrics) => void;
78
96
  }
97
+ /**
98
+ * Per-DoF camera control. Every setter records the measurement with the
99
+ * native steady_clock time of arrival and feeds it into an α-β tracker
100
+ * (constant-velocity predictor) that owns both demand and actual state.
101
+ *
102
+ * All setters are synchronous and thread-safe. Calling them from a
103
+ * Reanimated worklet via a Nitro `hybridRef` is the supported path for
104
+ * high-frequency updates; the worklet thread takes the integrator's mutex
105
+ * briefly and returns immediately.
106
+ *
107
+ * Getters return Promises and should be called from the JS thread (e.g. a
108
+ * `useEffect` or a throttled HUD update), not from a worklet.
109
+ */
79
110
  export interface CesiumViewMethods extends HybridViewMethods {
80
- /** Returns the current native camera state. */
81
- getCameraState(): Promise<CameraState>;
82
111
  /**
83
- * Runtime camera control (heading/pitch/roll + position + VFOV).
84
- * Does not change the view-correction quaternion; use `setCameraQuaternion` to set that.
112
+ * New geographic position demand (latitude / longitude in degrees). Use
113
+ * for GPS or scripted position updates. Latitude is clamped to ±90 by the
114
+ * integrator; longitude is continuous (no wrap normalisation) so the
115
+ * shortest-arc residual maths stays correct across the antimeridian.
116
+ */
117
+ setPosition(latitude: number, longitude: number): void;
118
+ /** New altitude demand (metres above mean sea level). */
119
+ setAltitude(altitudeMeters: number): void;
120
+ /** New heading demand (degrees, 0 = north, increasing clockwise). */
121
+ setHeading(headingDeg: number): void;
122
+ /**
123
+ * New attitude demand (pitch and roll in degrees). Bundled because almost
124
+ * every IMU emits them together — but you can pass the previous value for
125
+ * either axis if you only have one of them.
126
+ */
127
+ setAttitude(pitchDeg: number, rollDeg: number): void;
128
+ /**
129
+ * New camera-space rotation applied after HPR (unit quaternion). Use for
130
+ * boresight calibration or screen-fixed HUD alignment. SLERPed toward the
131
+ * latest demand each frame.
132
+ */
133
+ setViewCorrection(q: Quaternion): void;
134
+ /** New vertical field-of-view demand (degrees). Clamped to 20..100. */
135
+ setVerticalFov(deg: number): void;
136
+ /**
137
+ * Atomically reset every DoF to the given camera state (value, velocity
138
+ * and demand all snap to the target). Bypasses the integrator entirely;
139
+ * use for `Fly to coordinate` actions or initial seeding from saved
140
+ * state.
141
+ */
142
+ teleport(camera: CameraState): void;
143
+ /**
144
+ * Most recent camera that was rendered (post-integration). This is what
145
+ * the user is currently looking at and is what a HUD overlay should
146
+ * mirror. Differs from `getDemandCamera()` while a glide is in progress
147
+ * or when the terrain-floor clamp has raised the altitude.
85
148
  */
86
- setCamera(camera: CameraState): void;
149
+ getActualCamera(): Promise<CameraState>;
87
150
  /**
88
- * Same fields as `setCamera`, plus a camera-space rotation applied after HPR
89
- * (e.g. boresight / HUD alignment). See README.
151
+ * What the consumer last asked for (per-DoF demand). Useful for
152
+ * diagnostics or for showing the requested camera alongside the rendered
153
+ * one.
90
154
  */
91
- setCameraQuaternion(camera: CameraState, viewCorrection: Quaternion): void;
92
- /** Current view-correction quaternion (identity if never set via `setCameraQuaternion`). */
155
+ getDemandCamera(): Promise<CameraState>;
156
+ /** Current view-correction quaternion (smoothed toward the latest demand). */
93
157
  getViewCorrection(): Promise<Quaternion>;
94
158
  }
95
159
  export type CesiumView = HybridView<CesiumViewProps, CesiumViewMethods>;
@@ -1 +1 @@
1
- {"version":3,"file":"cesium-view.nitro.d.ts","sourceRoot":"","sources":["../../../../src/specs/cesium-view.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,eAAe,EACf,iBAAiB,EAClB,MAAM,4BAA4B,CAAA;AAEnC,6EAA6E;AAC7E,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,cAAc,EAAE,MAAM,CAAA;CACvB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV;AAED,iEAAiE;AACjE,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;IACpB,kBAAkB,EAAE,OAAO,CAAA;IAC3B,+DAA+D;IAC/D,aAAa,EAAE,OAAO,CAAA;IACtB,YAAY,EAAE,OAAO,CAAA;IACrB,gBAAgB,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,eAAgB,SAAQ,eAAe;IACtD,cAAc,EAAE,MAAM,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,4EAA4E;IAC5E,aAAa,EAAE,WAAW,CAAA;IAC1B,cAAc,EAAE,OAAO,CAAA;IACvB,uBAAuB,EAAE,MAAM,CAAA;IAC/B,4BAA4B,EAAE,MAAM,CAAA;IACpC,sBAAsB,EAAE,MAAM,CAAA;IAC9B,6CAA6C;IAC7C,eAAe,EAAE,MAAM,CAAA;IACvB,iBAAiB,EAAE,MAAM,CAAA;IAGzB,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,mGAAmG;IACnG,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,mEAAmE;IACnE,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,gFAAgF;IAChF,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,oEAAoE;IACpE,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,2EAA2E;IAC3E,6BAA6B,CAAC,EAAE,OAAO,CAAA;IACvC,4FAA4F;IAC5F,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B,8EAA8E;IAC9E,yBAAyB,CAAC,EAAE,OAAO,CAAA;IACnC,mDAAmD;IACnD,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,mDAAmD;IACnD,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,4EAA4E;IAC5E,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B;;;;;OAKG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAEhC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;CAC7C;AAED,MAAM,WAAW,iBAAkB,SAAQ,iBAAiB;IAC1D,+CAA+C;IAC/C,cAAc,IAAI,OAAO,CAAC,WAAW,CAAC,CAAA;IACtC;;;OAGG;IACH,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAA;IACpC;;;OAGG;IACH,mBAAmB,CAAC,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,UAAU,GAAG,IAAI,CAAA;IAC1E,4FAA4F;IAC5F,iBAAiB,IAAI,OAAO,CAAC,UAAU,CAAC,CAAA;CACzC;AAED,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,eAAe,EAAE,iBAAiB,CAAC,CAAA"}
1
+ {"version":3,"file":"cesium-view.nitro.d.ts","sourceRoot":"","sources":["../../../../src/specs/cesium-view.nitro.ts"],"names":[],"mappings":"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"}