react-native-cesium 1.2.3 → 1.2.5
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.
- package/README.md +100 -27
- package/android/CMakeLists.txt +1 -2
- package/android/src/main/cpp/CesiumBridgeAndroid.cpp +264 -130
- package/android/src/main/cpp/CesiumBridgeAndroid.h +70 -27
- package/android/src/main/java/com/margelo/nitro/reactnativecesium/CesiumBridgeJNI.kt +68 -44
- package/android/src/main/java/com/margelo/nitro/reactnativecesium/HybridCesiumView.kt +199 -187
- package/cpp/engine/CameraIntegrator.cpp +374 -0
- package/cpp/engine/CameraIntegrator.hpp +146 -0
- package/cpp/engine/EngineTunables.hpp +90 -31
- package/cpp/engine/GlobeCamera.cpp +17 -6
- package/cpp/engine/GlobeCamera.hpp +11 -8
- package/cpp/engine/test/CameraIntegratorTest.cpp +205 -0
- package/cpp/engine/test/build_and_run.sh +27 -0
- package/example/README.md +1 -1
- package/ios/CesiumBridge.h +53 -28
- package/ios/CesiumBridge.mm +172 -86
- package/ios/HybridCesiumView.swift +256 -145
- package/lib/typescript/src/specs/cesium-view.nitro.d.ts +85 -13
- package/lib/typescript/src/specs/cesium-view.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/ReactNativeCesiumOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_CameraState.hpp +77 -0
- package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.cpp +108 -9
- package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.hpp +21 -3
- package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.cpp +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/Func_void_CameraState.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/HybridCesiumViewSpec.kt +71 -3
- package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Bridge.cpp +3 -3
- package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Bridge.hpp +43 -28
- package/nitrogen/generated/ios/c++/HybridCesiumViewSpecSwift.hpp +89 -9
- package/nitrogen/generated/ios/c++/views/HybridCesiumViewComponent.mm +30 -0
- package/nitrogen/generated/ios/swift/Func_void_CameraState.swift +5 -5
- package/nitrogen/generated/ios/swift/HybridCesiumViewSpec.swift +15 -3
- package/nitrogen/generated/ios/swift/HybridCesiumViewSpec_cxx.swift +242 -16
- package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.cpp +21 -3
- package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.hpp +22 -4
- package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.cpp +66 -0
- package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.hpp +6 -0
- package/nitrogen/generated/shared/json/CesiumViewConfig.json +6 -0
- package/package.json +1 -1
- package/src/specs/cesium-view.nitro.ts +102 -14
- package/cpp/engine/CameraSmoother.cpp +0 -68
- package/cpp/engine/CameraSmoother.hpp +0 -28
- package/cpp/engine/CameraTargetState.cpp +0 -205
- package/cpp/engine/CameraTargetState.hpp +0 -126
|
@@ -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
|
|
26
|
-
/// is created; subsequent prop writes are ignored.
|
|
27
|
-
///
|
|
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,21 +114,80 @@ 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
|
|
112
|
-
///
|
|
113
|
-
///
|
|
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
|
}
|
|
117
145
|
|
|
118
146
|
var onMetrics: ((CesiumMetrics) -> Void)?
|
|
119
147
|
|
|
148
|
+
var onActualCamera: ((CameraState) -> Void)?
|
|
149
|
+
// Both accessed only on the render queue; no extra locking needed.
|
|
150
|
+
private var actualCameraLastEmitTime: CFTimeInterval = 0
|
|
151
|
+
private var actualCameraLastSent: CameraState? = nil
|
|
152
|
+
|
|
153
|
+
// Returns true when the two snapshots differ enough to warrant a JS dispatch.
|
|
154
|
+
// Thresholds mirror EngineTunables.hpp kActualCameraCallbackInterval* constants.
|
|
155
|
+
private func actualCameraChanged(_ a: CameraState, from b: CameraState) -> Bool {
|
|
156
|
+
abs(a.latitude - b.latitude) > 1e-7 ||
|
|
157
|
+
abs(a.longitude - b.longitude) > 1e-7 ||
|
|
158
|
+
abs(a.altitude - b.altitude) > 0.05 ||
|
|
159
|
+
abs(a.heading - b.heading) > 0.01 ||
|
|
160
|
+
abs(a.pitch - b.pitch) > 0.01 ||
|
|
161
|
+
abs(a.roll - b.roll) > 0.01 ||
|
|
162
|
+
abs(a.verticalFovDeg - b.verticalFovDeg) > 0.01
|
|
163
|
+
}
|
|
164
|
+
|
|
120
165
|
// MARK: - Methods
|
|
121
166
|
|
|
122
|
-
func
|
|
167
|
+
func getActualCamera() throws -> Promise<CameraState> {
|
|
168
|
+
guard let b = bridge else {
|
|
169
|
+
return Promise.rejected(
|
|
170
|
+
withError: NSError(
|
|
171
|
+
domain: "HybridCesiumView",
|
|
172
|
+
code: 1,
|
|
173
|
+
userInfo: [NSLocalizedDescriptionKey: "Native bridge not initialized"]
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
return Promise.resolved(
|
|
178
|
+
withResult: CameraState(
|
|
179
|
+
latitude: b.readActualLatitude(),
|
|
180
|
+
longitude: b.readActualLongitude(),
|
|
181
|
+
altitude: b.readActualAltitude(),
|
|
182
|
+
heading: b.readActualHeading(),
|
|
183
|
+
pitch: b.readActualPitch(),
|
|
184
|
+
roll: b.readActualRoll(),
|
|
185
|
+
verticalFovDeg: b.readActualVerticalFovDeg()
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
func getDemandCamera() throws -> Promise<CameraState> {
|
|
123
191
|
guard let b = bridge else {
|
|
124
192
|
return Promise.rejected(
|
|
125
193
|
withError: NSError(
|
|
@@ -129,47 +197,79 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
129
197
|
)
|
|
130
198
|
)
|
|
131
199
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
200
|
+
return Promise.resolved(
|
|
201
|
+
withResult: CameraState(
|
|
202
|
+
latitude: b.readDemandLatitude(),
|
|
203
|
+
longitude: b.readDemandLongitude(),
|
|
204
|
+
altitude: b.readDemandAltitude(),
|
|
205
|
+
heading: b.readDemandHeading(),
|
|
206
|
+
pitch: b.readDemandPitch(),
|
|
207
|
+
roll: b.readDemandRoll(),
|
|
208
|
+
verticalFovDeg: b.readDemandVerticalFovDeg()
|
|
209
|
+
)
|
|
140
210
|
)
|
|
141
|
-
return Promise.resolved(withResult: s)
|
|
142
211
|
}
|
|
143
212
|
|
|
144
|
-
func
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
213
|
+
func setPosition(latitude: Double, longitude: Double) throws {
|
|
214
|
+
if let b = bridge {
|
|
215
|
+
b.setPositionLatitude(latitude, longitude: longitude)
|
|
216
|
+
} else {
|
|
217
|
+
pendingLat = latitude
|
|
218
|
+
pendingLon = longitude
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
func setAltitude(altitudeMeters: Double) throws {
|
|
223
|
+
if let b = bridge { b.setAltitude(altitudeMeters) }
|
|
224
|
+
else { pendingAltitude = altitudeMeters }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
func setHeading(headingDeg: Double) throws {
|
|
228
|
+
if let b = bridge { b.setHeadingDeg(headingDeg) }
|
|
229
|
+
else { pendingHeading = headingDeg }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
func setAttitude(pitchDeg: Double, rollDeg: Double) throws {
|
|
233
|
+
if let b = bridge { b.setAttitudePitch(pitchDeg, roll: rollDeg) }
|
|
234
|
+
else { pendingPitch = pitchDeg; pendingRoll = rollDeg }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
func setViewCorrection(q: Quaternion) throws {
|
|
238
|
+
if let b = bridge { b.setViewCorrectionW(q.w, x: q.x, y: q.y, z: q.z) }
|
|
239
|
+
else { pendingViewCorrection = q }
|
|
152
240
|
}
|
|
153
241
|
|
|
154
|
-
func
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
242
|
+
func setVerticalFov(deg: Double) throws {
|
|
243
|
+
if let b = bridge { b.setVerticalFovDeg(deg) }
|
|
244
|
+
else { pendingVfov = deg }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
func teleport(camera: CameraState) throws {
|
|
248
|
+
if let b = bridge {
|
|
249
|
+
b.teleportLatitude(camera.latitude,
|
|
250
|
+
longitude: camera.longitude,
|
|
251
|
+
altitude: camera.altitude,
|
|
252
|
+
heading: camera.heading,
|
|
253
|
+
pitch: camera.pitch,
|
|
254
|
+
roll: camera.roll,
|
|
255
|
+
verticalFovDeg: camera.verticalFovDeg)
|
|
256
|
+
} else {
|
|
257
|
+
pendingTeleport = camera
|
|
258
|
+
}
|
|
158
259
|
}
|
|
159
260
|
|
|
160
261
|
func getViewCorrection() throws -> Promise<Quaternion> {
|
|
161
262
|
guard let b = bridge else {
|
|
162
|
-
return Promise.resolved(
|
|
163
|
-
withResult: Quaternion(w: 1, x: 0, y: 0, z: 0)
|
|
164
|
-
)
|
|
263
|
+
return Promise.resolved(withResult: Quaternion(w: 1, x: 0, y: 0, z: 0))
|
|
165
264
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
265
|
+
return Promise.resolved(
|
|
266
|
+
withResult: Quaternion(
|
|
267
|
+
w: b.readViewCorrectionW(),
|
|
268
|
+
x: b.readViewCorrectionX(),
|
|
269
|
+
y: b.readViewCorrectionY(),
|
|
270
|
+
z: b.readViewCorrectionZ()
|
|
271
|
+
)
|
|
171
272
|
)
|
|
172
|
-
return Promise.resolved(withResult: q)
|
|
173
273
|
}
|
|
174
274
|
|
|
175
275
|
// MARK: - View
|
|
@@ -177,36 +277,50 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
177
277
|
private let metalView: MTKView
|
|
178
278
|
private var bridge: CesiumBridge?
|
|
179
279
|
private var displayLink: CADisplayLink?
|
|
280
|
+
private let displayLinkProxy = DisplayLinkProxy()
|
|
180
281
|
private var layoutPollTimer: Timer?
|
|
181
282
|
private var metricsFrameCounter: Int = 0
|
|
182
283
|
private var idleProbeAccumulator: Double = 0
|
|
183
284
|
private var usingLowRefreshRate = false
|
|
184
285
|
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
286
|
private var lastBridgePixelSize: CGSize = .zero
|
|
192
287
|
|
|
288
|
+
/// Demand values that arrived before the bridge was created. Drained once
|
|
289
|
+
/// in `ensureInitialized()` after the seed teleport.
|
|
290
|
+
private var pendingTeleport: CameraState?
|
|
291
|
+
private var pendingLat: Double?
|
|
292
|
+
private var pendingLon: Double?
|
|
293
|
+
private var pendingAltitude: Double?
|
|
294
|
+
private var pendingHeading: Double?
|
|
295
|
+
private var pendingPitch: Double?
|
|
296
|
+
private var pendingRoll: Double?
|
|
297
|
+
private var pendingVfov: Double?
|
|
298
|
+
private var pendingViewCorrection: Quaternion?
|
|
299
|
+
|
|
193
300
|
/// Dedicated serial render queue. CADisplayLink fires on `.main`, but the
|
|
194
301
|
/// actual `bridge.shouldRenderNextFrame()` / `bridge.renderFrame(...)` work
|
|
195
302
|
/// is dispatched here so the main thread is never blocked on Cesium tile
|
|
196
303
|
/// loading or Metal command-buffer encoding. All bridge prop setters also
|
|
197
304
|
/// dispatch here so they serialize with rendering and never race with
|
|
198
|
-
/// engine state.
|
|
305
|
+
/// engine state. Per-DoF camera writes deliberately bypass this hop —
|
|
306
|
+
/// CameraIntegrator's internal mutex handles cross-thread safety.
|
|
199
307
|
private let renderQueue = DispatchQueue(
|
|
200
308
|
label: "com.margelo.cesium.render",
|
|
201
309
|
qos: .userInteractive
|
|
202
310
|
)
|
|
203
311
|
/// Drops a CADisplayLink tick if the render queue is still finishing the
|
|
204
|
-
/// previous frame
|
|
205
|
-
/// stutter (Metal's frameSemaphore would already throttle, but this saves
|
|
206
|
-
/// the dispatch hop and keeps `dt` accurate).
|
|
312
|
+
/// previous frame.
|
|
207
313
|
private var renderInFlight = false
|
|
208
314
|
private let renderInFlightLock = NSLock()
|
|
209
315
|
|
|
316
|
+
/// Set to true at the start of `deinit`. Any work still queued on the
|
|
317
|
+
/// render or main queues checks this before touching the bridge or
|
|
318
|
+
/// invoking JS callbacks. Avoids two classes of warnings during fast
|
|
319
|
+
/// refresh / unmount: Nitro "Dispatcher has already been destroyed"
|
|
320
|
+
/// when emitting `onMetrics` into a dying JS runtime, and stray frame
|
|
321
|
+
/// ticks running after the bridge has been torn down.
|
|
322
|
+
private var isShutdown = false
|
|
323
|
+
|
|
210
324
|
var view: UIView { metalView }
|
|
211
325
|
|
|
212
326
|
override init() {
|
|
@@ -270,17 +384,29 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
270
384
|
if !ionAccessToken.isEmpty {
|
|
271
385
|
bridge?.updateIonAccessToken(ionAccessToken, assetId: Int64(ionAssetId))
|
|
272
386
|
}
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
387
|
+
// Seed from initialCamera, then replay anything the consumer set before
|
|
388
|
+
// we were ready.
|
|
389
|
+
let seed = pendingTeleport ?? initialCamera
|
|
390
|
+
bridge?.teleportLatitude(seed.latitude,
|
|
391
|
+
longitude: seed.longitude,
|
|
392
|
+
altitude: seed.altitude,
|
|
393
|
+
heading: seed.heading,
|
|
394
|
+
pitch: seed.pitch,
|
|
395
|
+
roll: seed.roll,
|
|
396
|
+
verticalFovDeg: seed.verticalFovDeg)
|
|
397
|
+
pendingTeleport = nil
|
|
398
|
+
|
|
399
|
+
if let la = pendingLat, let lo = pendingLon {
|
|
400
|
+
bridge?.setPositionLatitude(la, longitude: lo); pendingLat = nil; pendingLon = nil
|
|
283
401
|
}
|
|
402
|
+
if let a = pendingAltitude { bridge?.setAltitude(a); pendingAltitude = nil }
|
|
403
|
+
if let h = pendingHeading { bridge?.setHeadingDeg(h); pendingHeading = nil }
|
|
404
|
+
if let p = pendingPitch, let r = pendingRoll {
|
|
405
|
+
bridge?.setAttitudePitch(p, roll: r); pendingPitch = nil; pendingRoll = nil
|
|
406
|
+
}
|
|
407
|
+
if let f = pendingVfov { bridge?.setVerticalFovDeg(f); pendingVfov = nil }
|
|
408
|
+
if let q = pendingViewCorrection { bridge?.setViewCorrectionW(q.w, x: q.x, y: q.y, z: q.z); pendingViewCorrection = nil }
|
|
409
|
+
|
|
284
410
|
if ionImageryAssetId != 1 {
|
|
285
411
|
bridge?.updateImageryAssetId(Int64(ionImageryAssetId))
|
|
286
412
|
}
|
|
@@ -289,6 +415,8 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
289
415
|
bridge?.setMsaaSampleCount(sc)
|
|
290
416
|
applyMtkViewMsaa(Int(sc))
|
|
291
417
|
|
|
418
|
+
pushRateCaps()
|
|
419
|
+
|
|
292
420
|
startRenderLoop()
|
|
293
421
|
}
|
|
294
422
|
|
|
@@ -325,69 +453,14 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
325
453
|
if let v = minAltitudeAboveTerrain { bridge?.setMinAltitudeAboveTerrain(Float(v)) }
|
|
326
454
|
}
|
|
327
455
|
|
|
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
456
|
// MARK: - Render Loop
|
|
388
457
|
|
|
389
458
|
private func startRenderLoop() {
|
|
390
|
-
|
|
459
|
+
displayLinkProxy.owner = self
|
|
460
|
+
displayLink = CADisplayLink(
|
|
461
|
+
target: displayLinkProxy,
|
|
462
|
+
selector: #selector(DisplayLinkProxy.tick)
|
|
463
|
+
)
|
|
391
464
|
setDisplayLinkFrameRate(idle: false)
|
|
392
465
|
displayLink?.add(to: .main, forMode: .common)
|
|
393
466
|
displayLink?.isPaused = pauseRendering
|
|
@@ -403,13 +476,12 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
403
476
|
: CAFrameRateRange(minimum: 30, maximum: 60, preferred: 60)
|
|
404
477
|
}
|
|
405
478
|
|
|
406
|
-
|
|
479
|
+
/// Invoked by `DisplayLinkProxy` on every CADisplayLink tick (main thread).
|
|
480
|
+
fileprivate func handleDisplayLinkTick() {
|
|
407
481
|
guard !pauseRendering,
|
|
408
|
-
|
|
482
|
+
!isShutdown,
|
|
409
483
|
let bridge else { return }
|
|
410
484
|
|
|
411
|
-
let dt = max(dl.targetTimestamp - dl.timestamp, 1.0 / 120.0)
|
|
412
|
-
|
|
413
485
|
// metalView.bounds / drawableSize are UIKit, must stay on main.
|
|
414
486
|
let scale = metalView.contentScaleFactor
|
|
415
487
|
let w = Int(metalView.bounds.width * scale)
|
|
@@ -424,9 +496,7 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
424
496
|
}
|
|
425
497
|
}
|
|
426
498
|
|
|
427
|
-
//
|
|
428
|
-
// frame yet. This keeps the dispatch backlog at zero and lets the next
|
|
429
|
-
// CADisplayLink fire pick up a fresh dt.
|
|
499
|
+
// Drop the tick if the render queue hasn't finished the previous frame yet.
|
|
430
500
|
renderInFlightLock.lock()
|
|
431
501
|
if renderInFlight {
|
|
432
502
|
renderInFlightLock.unlock()
|
|
@@ -435,16 +505,24 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
435
505
|
renderInFlight = true
|
|
436
506
|
renderInFlightLock.unlock()
|
|
437
507
|
|
|
508
|
+
// Wall-clock time used for the metrics-only frame-dt EMA inside the
|
|
509
|
+
// bridge. The camera integrator owns its own clock — see
|
|
510
|
+
// CameraIntegrator::step().
|
|
511
|
+
let nowSeconds = CACurrentMediaTime()
|
|
512
|
+
|
|
438
513
|
let resizedW = w
|
|
439
514
|
let resizedH = h
|
|
440
|
-
let weakSelf = self // captured for main-thread post-frame work
|
|
441
515
|
renderQueue.async { [weak self, weak bridge] in
|
|
442
|
-
guard let self = self
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
516
|
+
guard let self = self else { return }
|
|
517
|
+
// `renderInFlight` must always be cleared so a single dropped tick
|
|
518
|
+
// doesn't permanently wedge the render loop. defer guarantees it
|
|
519
|
+
// regardless of which early-return path we take below.
|
|
520
|
+
defer {
|
|
521
|
+
self.renderInFlightLock.lock()
|
|
522
|
+
self.renderInFlight = false
|
|
523
|
+
self.renderInFlightLock.unlock()
|
|
447
524
|
}
|
|
525
|
+
guard let bridge = bridge else { return }
|
|
448
526
|
|
|
449
527
|
if didResize {
|
|
450
528
|
bridge.resize(Int32(resizedW), height: Int32(resizedH))
|
|
@@ -455,12 +533,13 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
455
533
|
let nextIsIdle: Bool
|
|
456
534
|
if shouldRenderNow {
|
|
457
535
|
self.idleProbeAccumulator = 0
|
|
458
|
-
bridge.renderFrame(
|
|
536
|
+
bridge.renderFrame(at: nowSeconds)
|
|
459
537
|
nextIsIdle = false
|
|
460
538
|
} else {
|
|
461
|
-
|
|
539
|
+
// Approximate dt for the idle probe. We don't need precision here.
|
|
540
|
+
let probeDt = 1.0 / 60.0
|
|
541
|
+
self.idleProbeAccumulator += probeDt
|
|
462
542
|
if self.idleProbeAccumulator >= 0.25 {
|
|
463
|
-
// Safety probe: occasionally tick engine state for late tile completions.
|
|
464
543
|
self.idleProbeAccumulator = 0
|
|
465
544
|
bridge.markNeedsRender()
|
|
466
545
|
}
|
|
@@ -469,7 +548,7 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
469
548
|
|
|
470
549
|
self.metricsFrameCounter += 1
|
|
471
550
|
let shouldEmitMetrics =
|
|
472
|
-
self.metricsFrameCounter >= 20 && self.onMetrics != nil
|
|
551
|
+
self.metricsFrameCounter >= 20 && self.onMetrics != nil && !self.isShutdown
|
|
473
552
|
let metricsSnapshot: CesiumMetrics? = shouldEmitMetrics ? CesiumMetrics(
|
|
474
553
|
fps: bridge.metricsFps,
|
|
475
554
|
tilesRendered: Double(bridge.metricsTilesRendered),
|
|
@@ -482,32 +561,64 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
482
561
|
) : nil
|
|
483
562
|
if shouldEmitMetrics { self.metricsFrameCounter = 0 }
|
|
484
563
|
|
|
485
|
-
//
|
|
486
|
-
//
|
|
564
|
+
// onActualCamera: poll at 5 Hz (kActualCameraCallbackIntervalSec = 0.2 s),
|
|
565
|
+
// but only dispatch when at least one field has moved beyond its epsilon.
|
|
566
|
+
var actualCameraSnapshot: CameraState? = nil
|
|
567
|
+
if self.onActualCamera != nil && !self.isShutdown &&
|
|
568
|
+
(nowSeconds - self.actualCameraLastEmitTime) >= 0.2 {
|
|
569
|
+
self.actualCameraLastEmitTime = nowSeconds
|
|
570
|
+
let candidate = CameraState(
|
|
571
|
+
latitude: bridge.readActualLatitude(),
|
|
572
|
+
longitude: bridge.readActualLongitude(),
|
|
573
|
+
altitude: bridge.readActualAltitude(),
|
|
574
|
+
heading: bridge.readActualHeading(),
|
|
575
|
+
pitch: bridge.readActualPitch(),
|
|
576
|
+
roll: bridge.readActualRoll(),
|
|
577
|
+
verticalFovDeg: bridge.readActualVerticalFovDeg()
|
|
578
|
+
)
|
|
579
|
+
if self.actualCameraLastSent.map({ self.actualCameraChanged(candidate, from: $0) }) ?? true {
|
|
580
|
+
actualCameraSnapshot = candidate
|
|
581
|
+
self.actualCameraLastSent = candidate
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
487
585
|
DispatchQueue.main.async { [weak self] in
|
|
488
|
-
guard let self = self else { return }
|
|
586
|
+
guard let self = self, !self.isShutdown else { return }
|
|
489
587
|
self.setDisplayLinkFrameRate(idle: nextIsIdle)
|
|
490
588
|
if let m = metricsSnapshot, let cb = self.onMetrics {
|
|
491
589
|
cb(m)
|
|
492
590
|
}
|
|
591
|
+
if let cam = actualCameraSnapshot, let cb = self.onActualCamera {
|
|
592
|
+
cb(cam)
|
|
593
|
+
}
|
|
493
594
|
}
|
|
494
|
-
|
|
495
|
-
self.renderInFlightLock.lock()
|
|
496
|
-
self.renderInFlight = false
|
|
497
|
-
self.renderInFlightLock.unlock()
|
|
595
|
+
// renderInFlight is cleared by the defer at the top of this closure.
|
|
498
596
|
}
|
|
499
597
|
}
|
|
500
598
|
|
|
501
599
|
deinit {
|
|
600
|
+
// Set the shutdown gate first so any in-flight render-queue tick and
|
|
601
|
+
// any queued main-queue follow-on (metric emission, frame-rate update)
|
|
602
|
+
// bail out before they touch the bridge or invoke onMetrics into a
|
|
603
|
+
// JS dispatcher that may already be gone.
|
|
604
|
+
isShutdown = true
|
|
605
|
+
onMetrics = nil
|
|
606
|
+
onActualCamera = nil
|
|
607
|
+
actualCameraLastSent = nil
|
|
608
|
+
|
|
502
609
|
layoutPollTimer?.invalidate()
|
|
610
|
+
layoutPollTimer = nil
|
|
503
611
|
displayLink?.invalidate()
|
|
612
|
+
displayLink = nil
|
|
613
|
+
displayLinkProxy.owner = nil
|
|
504
614
|
|
|
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
615
|
let bridgeRef = bridge
|
|
509
616
|
bridge = nil
|
|
510
617
|
if let b = bridgeRef {
|
|
618
|
+
// Drain anything already on the render queue, then ask the bridge
|
|
619
|
+
// to release its engine + Metal backend synchronously. We do this
|
|
620
|
+
// inside the sync hop so the SqliteCache file handle is closed
|
|
621
|
+
// before any subsequent CesiumView mount can open the same cache.
|
|
511
622
|
renderQueue.sync {
|
|
512
623
|
b.shutdown()
|
|
513
624
|
}
|