react-native-cesium 1.0.6 → 1.2.0
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 +64 -11
- package/ReactNativeCesium.podspec +1 -1
- package/android/CMakeLists.txt +5 -3
- package/android/src/main/cpp/CesiumBridgeAndroid.cpp +197 -227
- package/android/src/main/cpp/CesiumBridgeAndroid.h +48 -49
- package/android/src/main/cpp/cpp-adapter.cpp +2 -2
- package/android/src/main/cpp/vulkan/VulkanBackend.cpp +1010 -569
- package/android/src/main/cpp/vulkan/VulkanBackend.h +121 -12
- package/android/src/main/cpp/vulkan/shaders/sky.frag +20 -10
- package/android/src/main/java/com/margelo/nitro/reactnativecesium/CesiumBridgeJNI.kt +28 -0
- package/android/src/main/java/com/margelo/nitro/reactnativecesium/HybridCesiumView.kt +295 -58
- package/cpp/engine/CameraSmoother.cpp +99 -0
- package/cpp/engine/CameraSmoother.hpp +29 -0
- package/cpp/engine/CameraTargetState.cpp +49 -0
- package/cpp/engine/CameraTargetState.hpp +121 -0
- package/cpp/engine/CesiumEngine.cpp +206 -60
- package/cpp/engine/CesiumEngine.hpp +77 -10
- package/cpp/engine/EngineTunables.hpp +79 -0
- package/cpp/engine/GeoidConverter.cpp +36 -0
- package/cpp/engine/GeoidConverter.hpp +10 -0
- package/cpp/engine/GlobeCamera.cpp +7 -2
- package/cpp/engine/MetricsAggregator.cpp +71 -0
- package/cpp/engine/MetricsAggregator.hpp +54 -0
- package/cpp/engine/TileLifecycleManager.cpp +0 -12
- package/cpp/engine/TileLifecycleManager.hpp +0 -3
- package/cpp/third_party/egm96/EGM96.c +289 -0
- package/cpp/third_party/egm96/EGM96.h +45 -0
- package/cpp/third_party/egm96/EGM96_data.h +65372 -0
- package/ios/CesiumBridge.h +24 -5
- package/ios/CesiumBridge.mm +149 -271
- package/ios/HybridCesiumView.swift +197 -40
- package/ios/metal/MetalBackend.mm +72 -23
- package/lib/typescript/src/specs/cesium-view.nitro.d.ts +26 -0
- package/lib/typescript/src/specs/cesium-view.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JCesiumMetrics.hpp +5 -1
- package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.cpp +109 -1
- package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.hpp +24 -0
- package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.cpp +48 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/CameraState.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/CesiumMetrics.kt +32 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/HybridCesiumViewSpec.kt +72 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/Quaternion.kt +19 -0
- package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Bridge.hpp +30 -0
- package/nitrogen/generated/ios/c++/HybridCesiumViewSpecSwift.hpp +85 -1
- package/nitrogen/generated/ios/c++/views/HybridCesiumViewComponent.mm +60 -0
- package/nitrogen/generated/ios/swift/CesiumMetrics.swift +7 -2
- package/nitrogen/generated/ios/swift/HybridCesiumViewSpec.swift +12 -0
- package/nitrogen/generated/ios/swift/HybridCesiumViewSpec_cxx.swift +288 -0
- package/nitrogen/generated/shared/c++/CesiumMetrics.hpp +5 -1
- package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.cpp +24 -0
- package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.hpp +25 -1
- package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.cpp +132 -0
- package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.hpp +13 -1
- package/nitrogen/generated/shared/json/CesiumViewConfig.json +12 -0
- package/package.json +4 -4
- package/scripts/cesium/clone.mjs +1 -1
- package/src/specs/cesium-view.nitro.ts +29 -0
- package/cpp/engine/DeletionQueue.cpp +0 -28
- package/cpp/engine/DeletionQueue.hpp +0 -28
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
package com.margelo.nitro.reactnativecesium
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.os.Handler
|
|
5
|
+
import android.os.HandlerThread
|
|
6
|
+
import android.os.Process
|
|
7
|
+
import android.util.Log
|
|
4
8
|
import android.view.Choreographer
|
|
5
9
|
import android.view.SurfaceHolder
|
|
6
10
|
import android.view.SurfaceView
|
|
@@ -8,28 +12,53 @@ import android.view.View
|
|
|
8
12
|
import com.margelo.nitro.core.Promise
|
|
9
13
|
import java.io.File
|
|
10
14
|
import java.io.FileOutputStream
|
|
15
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
11
16
|
|
|
12
17
|
class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec() {
|
|
13
18
|
private val surfaceView = SurfaceView(appContext)
|
|
14
|
-
|
|
19
|
+
/// Bridge is only ever read on the render thread. The UI thread posts
|
|
20
|
+
/// requests via `renderHandler`. Marked volatile so init/shutdown observers
|
|
21
|
+
/// (`bridge != null`) on the UI thread see the latest reference, but real
|
|
22
|
+
/// work is always serialized through the render thread.
|
|
23
|
+
@Volatile private var bridge: CesiumBridgeJNI? = null
|
|
15
24
|
private var metricsFrameCounter = 0
|
|
16
25
|
private var idleProbeAccumulator = 0.0
|
|
17
|
-
private var usingLowRefreshRate = false
|
|
18
26
|
private var lastFrameTimeNanos = 0L
|
|
27
|
+
/// Last-pushed camera state — read/written on the render thread (via
|
|
28
|
+
/// `renderHandler.post`), never directly on the UI thread.
|
|
19
29
|
private var lastPushedCamera: CameraState? = null
|
|
20
30
|
private var lastPushedViewCorrection: Quaternion? = null
|
|
31
|
+
/// Camera state requested by `setCamera` before the bridge was ready.
|
|
32
|
+
/// Updated on the UI thread, drained on the render thread.
|
|
33
|
+
@Volatile private var pendingRuntimeCamera: CameraState? = null
|
|
34
|
+
@Volatile private var pendingRuntimeViewCorrection: Quaternion? = null
|
|
35
|
+
/// True while a Choreographer frame callback is pending. Mutated on the UI
|
|
36
|
+
/// thread inside the callback / scheduleNextFrame.
|
|
21
37
|
private var renderLoopActive = false
|
|
38
|
+
/// Drops Choreographer ticks while the render thread is still finishing the
|
|
39
|
+
/// previous frame. Set on UI thread when posting; cleared on render thread
|
|
40
|
+
/// after the bridge call completes.
|
|
41
|
+
private val renderInFlight = AtomicBoolean(false)
|
|
42
|
+
|
|
43
|
+
/// Dedicated render thread — owns all CesiumEngine + GPU work. Created lazily
|
|
44
|
+
/// when the first SurfaceHolder.Callback fires. The Choreographer (UI thread)
|
|
45
|
+
/// posts render requests here; bridge config setters likewise post here so
|
|
46
|
+
/// they serialize with rendering and never race with engine state.
|
|
47
|
+
private var renderThread: HandlerThread? = null
|
|
48
|
+
private var renderHandler: Handler? = null
|
|
22
49
|
|
|
23
50
|
override var ionAccessToken: String = ""
|
|
24
51
|
set(value) {
|
|
25
52
|
field = value
|
|
26
|
-
|
|
53
|
+
val assetId = ionAssetId.toLong()
|
|
54
|
+
postToRender { bridge?.updateIonAccessToken(value, assetId) }
|
|
27
55
|
}
|
|
28
56
|
|
|
29
57
|
override var ionAssetId: Double = 1.0
|
|
30
58
|
set(value) {
|
|
31
59
|
field = value
|
|
32
|
-
|
|
60
|
+
val token = ionAccessToken
|
|
61
|
+
postToRender { bridge?.updateIonAccessToken(token, value.toLong()) }
|
|
33
62
|
}
|
|
34
63
|
|
|
35
64
|
override var initialCamera: CameraState = CameraState(
|
|
@@ -46,25 +75,51 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
|
|
|
46
75
|
set(value) {
|
|
47
76
|
field = value
|
|
48
77
|
if (!value) {
|
|
49
|
-
bridge?.markNeedsRender()
|
|
50
|
-
scheduleNextFrame()
|
|
78
|
+
postToRender { bridge?.markNeedsRender() }
|
|
79
|
+
scheduleNextFrame(immediate = true)
|
|
51
80
|
}
|
|
52
81
|
}
|
|
53
82
|
|
|
54
83
|
override var maximumScreenSpaceError: Double = 32.0
|
|
55
|
-
set(value) { field = value; bridge?.setMaxSSE(value) }
|
|
84
|
+
set(value) { field = value; postToRender { bridge?.setMaxSSE(value) } }
|
|
56
85
|
|
|
57
86
|
override var maximumSimultaneousTileLoads: Double = 12.0
|
|
58
|
-
set(value) { field = value; bridge?.setMaxSimLoads(value.toInt()) }
|
|
87
|
+
set(value) { field = value; postToRender { bridge?.setMaxSimLoads(value.toInt()) } }
|
|
59
88
|
|
|
60
89
|
override var loadingDescendantLimit: Double = 20.0
|
|
61
|
-
set(value) { field = value; bridge?.setLoadDescLim(value.toInt()) }
|
|
90
|
+
set(value) { field = value; postToRender { bridge?.setLoadDescLim(value.toInt()) } }
|
|
62
91
|
|
|
63
92
|
override var msaaSampleCount: Double = 1.0
|
|
64
|
-
set(value) { field = value; bridge?.setMsaa(value.toInt()) }
|
|
93
|
+
set(value) { field = value; postToRender { bridge?.setMsaa(value.toInt()) } }
|
|
65
94
|
|
|
66
95
|
override var ionImageryAssetId: Double = 1.0
|
|
67
|
-
set(value) { field = value; bridge?.updateImageryAssetId(value.toLong()) }
|
|
96
|
+
set(value) { field = value; postToRender { bridge?.updateImageryAssetId(value.toLong()) } }
|
|
97
|
+
|
|
98
|
+
// ── Optional perf / quality knobs ────────────────────────────────────────
|
|
99
|
+
override var maximumCachedMiB: Double? = null
|
|
100
|
+
set(value) { field = value; if (value != null) postToRender { bridge?.setMaximumCachedMiB(value.toInt()) } }
|
|
101
|
+
override var preloadAncestors: Boolean? = null
|
|
102
|
+
set(value) { field = value; if (value != null) postToRender { bridge?.setPreloadAncestors(value) } }
|
|
103
|
+
override var preloadSiblings: Boolean? = null
|
|
104
|
+
set(value) { field = value; if (value != null) postToRender { bridge?.setPreloadSiblings(value) } }
|
|
105
|
+
override var forbidHoles: Boolean? = null
|
|
106
|
+
set(value) { field = value; if (value != null) postToRender { bridge?.setForbidHoles(value) } }
|
|
107
|
+
override var enableWaterMask: Boolean? = null
|
|
108
|
+
set(value) { field = value; if (value != null) postToRender { bridge?.setEnableWaterMask(value) } }
|
|
109
|
+
override var enableFogCulling: Boolean? = null
|
|
110
|
+
set(value) { field = value; if (value != null) postToRender { bridge?.setEnableFogCulling(value) } }
|
|
111
|
+
override var enforceCulledScreenSpaceError: Boolean? = null
|
|
112
|
+
set(value) { field = value; if (value != null) postToRender { bridge?.setEnforceCulledScreenSpaceError(value) } }
|
|
113
|
+
override var culledScreenSpaceError: Double? = null
|
|
114
|
+
set(value) { field = value; if (value != null) postToRender { bridge?.setCulledScreenSpaceError(value) } }
|
|
115
|
+
override var enableLodTransitionPeriod: Boolean? = null
|
|
116
|
+
set(value) { field = value; if (value != null) postToRender { bridge?.setEnableLodTransitionPeriod(value) } }
|
|
117
|
+
override var lodTransitionLength: Double? = null
|
|
118
|
+
set(value) { field = value; if (value != null) postToRender { bridge?.setLodTransitionLength(value) } }
|
|
119
|
+
override var sqliteCacheMaxRows: Double? = null
|
|
120
|
+
set(value) { field = value; if (value != null) postToRender { bridge?.setSqliteCacheMaxRows(value.toInt()) } }
|
|
121
|
+
override var taskProcessorThreads: Double? = null
|
|
122
|
+
set(value) { field = value; if (value != null) postToRender { bridge?.setTaskProcessorThreads(value.toInt()) } }
|
|
68
123
|
|
|
69
124
|
override var onMetrics: ((metrics: CesiumMetrics) -> Unit)? = null
|
|
70
125
|
|
|
@@ -72,7 +127,9 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
|
|
|
72
127
|
|
|
73
128
|
private val frameCallback = Choreographer.FrameCallback { frameTimeNanos ->
|
|
74
129
|
renderLoopActive = false
|
|
75
|
-
if (pauseRendering || bridge == null)
|
|
130
|
+
if (pauseRendering || bridge == null) {
|
|
131
|
+
return@FrameCallback
|
|
132
|
+
}
|
|
76
133
|
|
|
77
134
|
val dt = if (lastFrameTimeNanos > 0) {
|
|
78
135
|
(frameTimeNanos - lastFrameTimeNanos).coerceAtLeast(1_000_000L) / 1_000_000_000.0
|
|
@@ -81,37 +138,71 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
|
|
|
81
138
|
}
|
|
82
139
|
lastFrameTimeNanos = frameTimeNanos
|
|
83
140
|
|
|
84
|
-
|
|
141
|
+
// Drop the tick if the previous frame's render hasn't finished — keeps the
|
|
142
|
+
// render thread from queuing up an unbounded backlog under stutter.
|
|
143
|
+
if (!renderInFlight.compareAndSet(false, true)) {
|
|
144
|
+
// Re-arm so we try again next vsync.
|
|
145
|
+
scheduleNextFrame(immediate = true)
|
|
146
|
+
return@FrameCallback
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
val rh = renderHandler
|
|
150
|
+
if (rh == null) {
|
|
151
|
+
renderInFlight.set(false)
|
|
152
|
+
return@FrameCallback
|
|
153
|
+
}
|
|
154
|
+
rh.post { runFrameOnRenderThread(dt) }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// Called on the render thread. Owns ALL CesiumEngine + GPU access; the UI
|
|
158
|
+
/// thread only signals demand here. Safe to read `onMetrics` because Nitro
|
|
159
|
+
/// callbacks dispatch internally to the JS thread.
|
|
160
|
+
private fun runFrameOnRenderThread(dt: Double) {
|
|
161
|
+
val b = bridge
|
|
162
|
+
if (b == null) {
|
|
163
|
+
renderInFlight.set(false)
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
val shouldRender = b.shouldRenderNextFrame()
|
|
168
|
+
val nextImmediate: Boolean
|
|
85
169
|
if (shouldRender) {
|
|
86
|
-
usingLowRefreshRate = false
|
|
87
170
|
idleProbeAccumulator = 0.0
|
|
88
|
-
|
|
171
|
+
b.renderFrame(dt)
|
|
172
|
+
nextImmediate = true
|
|
89
173
|
} else {
|
|
90
|
-
usingLowRefreshRate = true
|
|
91
174
|
idleProbeAccumulator += dt
|
|
92
175
|
if (idleProbeAccumulator >= 0.25) {
|
|
93
176
|
idleProbeAccumulator = 0.0
|
|
94
|
-
|
|
177
|
+
b.markNeedsRender()
|
|
95
178
|
}
|
|
179
|
+
nextImmediate = false
|
|
96
180
|
}
|
|
97
181
|
|
|
98
182
|
metricsFrameCounter++
|
|
99
183
|
if (metricsFrameCounter >= 20) {
|
|
100
184
|
metricsFrameCounter = 0
|
|
101
|
-
val
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
185
|
+
val cb = onMetrics
|
|
186
|
+
if (cb != null) {
|
|
187
|
+
cb(CesiumMetrics(
|
|
188
|
+
fps = b.getMetricsFps(),
|
|
189
|
+
tilesRendered = b.getMetricsTilesRendered().toDouble(),
|
|
190
|
+
tilesLoading = b.getMetricsTilesLoading().toDouble(),
|
|
191
|
+
tilesVisited = b.getMetricsTilesVisited().toDouble(),
|
|
192
|
+
ionTokenConfigured = b.getMetricsIonTokenConfigured(),
|
|
193
|
+
tlsConfigured = b.getMetricsTlsConfigured(),
|
|
194
|
+
tilesetReady = b.getMetricsTilesetReady(),
|
|
195
|
+
creditsPlainText = b.getMetricsCreditsPlainText(),
|
|
196
|
+
))
|
|
197
|
+
}
|
|
112
198
|
}
|
|
113
199
|
|
|
114
|
-
|
|
200
|
+
renderInFlight.set(false)
|
|
201
|
+
|
|
202
|
+
// Re-arm on the UI thread so Choreographer remains the source of truth
|
|
203
|
+
// for vsync timing. Posting a no-op runnable to surfaceView keeps us off
|
|
204
|
+
// the render thread for that small piece of bookkeeping.
|
|
205
|
+
surfaceView.post { scheduleNextFrame(immediate = nextImmediate) }
|
|
115
206
|
}
|
|
116
207
|
|
|
117
208
|
private val surfaceCallback = object : SurfaceHolder.Callback {
|
|
@@ -121,35 +212,109 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
|
|
|
121
212
|
val h = surfaceView.height
|
|
122
213
|
if (w <= 0 || h <= 0) return
|
|
123
214
|
|
|
124
|
-
|
|
215
|
+
// Spin up the render thread on first surfaceCreated. If we previously
|
|
216
|
+
// tore it down (e.g. on backgrounding), re-spawn cleanly.
|
|
217
|
+
ensureRenderThread()
|
|
218
|
+
val rh = renderHandler ?: return
|
|
219
|
+
|
|
125
220
|
val cacheDir = appContext.cacheDir.absolutePath
|
|
126
221
|
val cacertPath = extractCacert()
|
|
127
|
-
|
|
128
|
-
|
|
222
|
+
// Init runs on the render thread because Vulkan instance/device creation
|
|
223
|
+
// and Cesium Native init both touch state we'd rather only ever poke
|
|
224
|
+
// from one thread.
|
|
225
|
+
rh.post {
|
|
226
|
+
val b = CesiumBridgeJNI()
|
|
227
|
+
b.init(surface, w, h, cacheDir, cacertPath)
|
|
228
|
+
bridge = b
|
|
229
|
+
syncBridgePropsOnRenderThread(b)
|
|
230
|
+
|
|
231
|
+
b.updateCamera(
|
|
232
|
+
initialCamera.latitude, initialCamera.longitude, initialCamera.altitude,
|
|
233
|
+
initialCamera.heading, initialCamera.pitch, initialCamera.roll,
|
|
234
|
+
)
|
|
235
|
+
b.setVerticalFovDeg(initialCamera.verticalFovDeg)
|
|
236
|
+
lastPushedCamera = initialCamera
|
|
237
|
+
|
|
238
|
+
val pending = pendingRuntimeCamera
|
|
239
|
+
if (pending != null) {
|
|
240
|
+
val q = pendingRuntimeViewCorrection
|
|
241
|
+
if (q != null) {
|
|
242
|
+
b.updateCameraQuaternion(
|
|
243
|
+
pending.latitude, pending.longitude, pending.altitude,
|
|
244
|
+
pending.heading, pending.pitch, pending.roll,
|
|
245
|
+
q.w, q.x, q.y, q.z,
|
|
246
|
+
)
|
|
247
|
+
lastPushedViewCorrection = q
|
|
248
|
+
} else {
|
|
249
|
+
b.updateCamera(
|
|
250
|
+
pending.latitude, pending.longitude, pending.altitude,
|
|
251
|
+
pending.heading, pending.pitch, pending.roll,
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
b.setVerticalFovDeg(pending.verticalFovDeg)
|
|
255
|
+
lastPushedCamera = pending
|
|
256
|
+
}
|
|
129
257
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
b.
|
|
137
|
-
}
|
|
138
|
-
b.setMsaa(msaaSampleCount.toInt())
|
|
258
|
+
if (ionAccessToken.isNotEmpty()) {
|
|
259
|
+
b.updateIonAccessToken(ionAccessToken, ionAssetId.toLong())
|
|
260
|
+
}
|
|
261
|
+
if (ionImageryAssetId != 1.0) {
|
|
262
|
+
b.updateImageryAssetId(ionImageryAssetId.toLong())
|
|
263
|
+
}
|
|
264
|
+
b.setMsaa(msaaSampleCount.toInt())
|
|
139
265
|
|
|
140
|
-
|
|
141
|
-
|
|
266
|
+
// Start the rendering loop here, after the bridge is fully
|
|
267
|
+
// initialized. scheduleNextFrame must be invoked on the UI thread
|
|
268
|
+
// because Choreographer.postFrameCallback() requires it.
|
|
269
|
+
surfaceView.post { scheduleNextFrame(immediate = true) }
|
|
270
|
+
}
|
|
142
271
|
}
|
|
143
272
|
|
|
144
273
|
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
|
145
|
-
|
|
146
|
-
|
|
274
|
+
postToRender {
|
|
275
|
+
bridge?.resize(width, height)
|
|
276
|
+
bridge?.markNeedsRender()
|
|
277
|
+
// Re-arm the loop in case surfaceChanged fires before the bridge was
|
|
278
|
+
// created (rare, but possible on cold starts). Idempotent: scheduleNextFrame
|
|
279
|
+
// early-returns if the loop is already active.
|
|
280
|
+
surfaceView.post { scheduleNextFrame(immediate = true) }
|
|
281
|
+
}
|
|
147
282
|
}
|
|
148
283
|
|
|
149
284
|
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
285
|
+
val rh = renderHandler
|
|
286
|
+
val ht = renderThread
|
|
287
|
+
|
|
288
|
+
if (rh == null || ht == null) {
|
|
289
|
+
// Nothing to drain; clear bridge reference if it somehow exists.
|
|
290
|
+
bridge?.shutdown()
|
|
291
|
+
bridge?.destroy()
|
|
292
|
+
bridge = null
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Block the UI thread (briefly) until the render thread has shut the
|
|
297
|
+
// bridge down — surfaceDestroyed contract requires the surface to be
|
|
298
|
+
// released before this returns. We cap our wait so a stuck GPU teardown
|
|
299
|
+
// can't hang the UI thread indefinitely (matches our 1s shutdown
|
|
300
|
+
// semaphore policy on the Vulkan / Metal side).
|
|
301
|
+
val done = java.util.concurrent.CountDownLatch(1)
|
|
302
|
+
rh.post {
|
|
303
|
+
try {
|
|
304
|
+
bridge?.shutdown()
|
|
305
|
+
bridge?.destroy()
|
|
306
|
+
} catch (t: Throwable) {
|
|
307
|
+
Log.w("CesiumView", "render-thread shutdown threw", t)
|
|
308
|
+
} finally {
|
|
309
|
+
bridge = null
|
|
310
|
+
done.countDown()
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (!done.await(1500, java.util.concurrent.TimeUnit.MILLISECONDS)) {
|
|
314
|
+
Log.w("CesiumView", "Render-thread shutdown did not complete in 1.5s; "
|
|
315
|
+
+ "tearing down handler thread anyway.")
|
|
316
|
+
}
|
|
317
|
+
stopRenderThread()
|
|
153
318
|
}
|
|
154
319
|
}
|
|
155
320
|
|
|
@@ -157,6 +322,37 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
|
|
|
157
322
|
surfaceView.holder.addCallback(surfaceCallback)
|
|
158
323
|
}
|
|
159
324
|
|
|
325
|
+
// ── Render-thread helpers ────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
private fun ensureRenderThread() {
|
|
328
|
+
if (renderThread != null) return
|
|
329
|
+
val ht = HandlerThread("CesiumRender", Process.THREAD_PRIORITY_DISPLAY)
|
|
330
|
+
ht.start()
|
|
331
|
+
renderHandler = Handler(ht.looper)
|
|
332
|
+
renderThread = ht
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private fun stopRenderThread() {
|
|
336
|
+
val ht = renderThread ?: return
|
|
337
|
+
renderHandler = null
|
|
338
|
+
renderThread = null
|
|
339
|
+
ht.quitSafely()
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/// Posts an action to the render thread. When the render thread isn't
|
|
343
|
+
/// running yet (early prop assignment, or after teardown), we silently drop
|
|
344
|
+
/// the action — bridges always take their initial config from the
|
|
345
|
+
/// `syncBridgePropsOnRenderThread` step inside surfaceCreated.
|
|
346
|
+
private fun postToRender(action: () -> Unit) {
|
|
347
|
+
renderHandler?.post(action)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/// Camera reads cross the JNI from the JS thread (Nitro promise resolution).
|
|
351
|
+
/// They read snapshot scalar values through CesiumBridgeAndroid → engine →
|
|
352
|
+
/// GlobeCamera, none of which is mutated outside the render thread, so the
|
|
353
|
+
/// worst that can happen is observing a value from a frame slightly behind
|
|
354
|
+
/// what the render thread is encoding right now — acceptable for a UI-driven
|
|
355
|
+
/// camera read.
|
|
160
356
|
override fun getCameraState(): Promise<CameraState> {
|
|
161
357
|
val b = bridge ?: return Promise.resolved(initialCamera)
|
|
162
358
|
return Promise.resolved(CameraState(
|
|
@@ -171,13 +367,25 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
|
|
|
171
367
|
}
|
|
172
368
|
|
|
173
369
|
override fun setCamera(camera: CameraState) {
|
|
174
|
-
initialCamera
|
|
175
|
-
|
|
370
|
+
// Runtime camera control. Does NOT mutate `initialCamera` (which is
|
|
371
|
+
// construction-time only).
|
|
372
|
+
pendingRuntimeCamera = camera
|
|
373
|
+
pendingRuntimeViewCorrection = null
|
|
374
|
+
postToRender {
|
|
375
|
+
pushCameraIfChanged(camera)
|
|
376
|
+
bridge?.markNeedsRender()
|
|
377
|
+
}
|
|
378
|
+
scheduleNextFrame(immediate = true)
|
|
176
379
|
}
|
|
177
380
|
|
|
178
381
|
override fun setCameraQuaternion(camera: CameraState, viewCorrection: Quaternion) {
|
|
179
|
-
|
|
180
|
-
|
|
382
|
+
pendingRuntimeCamera = camera
|
|
383
|
+
pendingRuntimeViewCorrection = viewCorrection
|
|
384
|
+
postToRender {
|
|
385
|
+
pushCameraQuaternionIfChanged(camera, viewCorrection)
|
|
386
|
+
bridge?.markNeedsRender()
|
|
387
|
+
}
|
|
388
|
+
scheduleNextFrame(immediate = true)
|
|
181
389
|
}
|
|
182
390
|
|
|
183
391
|
override fun getViewCorrection(): Promise<Quaternion> {
|
|
@@ -248,16 +456,45 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
|
|
|
248
456
|
b.setVerticalFovDeg(camera.verticalFovDeg)
|
|
249
457
|
}
|
|
250
458
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
459
|
+
/// Run on the render thread immediately after the bridge is created, before
|
|
460
|
+
/// the first frame. Matches Swift's syncBridgeOptionsFromProps and pushes
|
|
461
|
+
/// every prop into the freshly-built CesiumEngine so the consumer doesn't
|
|
462
|
+
/// have to re-set them after the surface comes up.
|
|
463
|
+
private fun syncBridgePropsOnRenderThread(b: CesiumBridgeJNI) {
|
|
464
|
+
b.setMaxSSE(maximumScreenSpaceError)
|
|
465
|
+
b.setMaxSimLoads(maximumSimultaneousTileLoads.toInt())
|
|
466
|
+
b.setLoadDescLim(loadingDescendantLimit.toInt())
|
|
467
|
+
maximumCachedMiB?.let { b.setMaximumCachedMiB(it.toInt()) }
|
|
468
|
+
preloadAncestors?.let { b.setPreloadAncestors(it) }
|
|
469
|
+
preloadSiblings?.let { b.setPreloadSiblings(it) }
|
|
470
|
+
forbidHoles?.let { b.setForbidHoles(it) }
|
|
471
|
+
enableWaterMask?.let { b.setEnableWaterMask(it) }
|
|
472
|
+
enableFogCulling?.let { b.setEnableFogCulling(it) }
|
|
473
|
+
enforceCulledScreenSpaceError?.let { b.setEnforceCulledScreenSpaceError(it) }
|
|
474
|
+
culledScreenSpaceError?.let { b.setCulledScreenSpaceError(it) }
|
|
475
|
+
enableLodTransitionPeriod?.let { b.setEnableLodTransitionPeriod(it) }
|
|
476
|
+
lodTransitionLength?.let { b.setLodTransitionLength(it) }
|
|
477
|
+
sqliteCacheMaxRows?.let { b.setSqliteCacheMaxRows(it.toInt()) }
|
|
478
|
+
taskProcessorThreads?.let { b.setTaskProcessorThreads(it.toInt()) }
|
|
255
479
|
}
|
|
256
480
|
|
|
257
|
-
|
|
481
|
+
/**
|
|
482
|
+
* Schedule the next frame callback.
|
|
483
|
+
*
|
|
484
|
+
* - `immediate = true`: the engine wants to render this vsync (active pan,
|
|
485
|
+
* tile loading, prop change). Re-post on the next available vsync.
|
|
486
|
+
* - `immediate = false`: the engine is idle. Re-post in 250ms — matches the
|
|
487
|
+
* iOS low-refresh range and keeps the main thread mostly asleep while
|
|
488
|
+
* still picking up late tile completions / network responses.
|
|
489
|
+
*/
|
|
490
|
+
private fun scheduleNextFrame(immediate: Boolean) {
|
|
258
491
|
if (renderLoopActive || pauseRendering || bridge == null) return
|
|
259
492
|
renderLoopActive = true
|
|
260
|
-
|
|
493
|
+
if (immediate) {
|
|
494
|
+
Choreographer.getInstance().postFrameCallback(frameCallback)
|
|
495
|
+
} else {
|
|
496
|
+
Choreographer.getInstance().postFrameCallbackDelayed(frameCallback, 250L)
|
|
497
|
+
}
|
|
261
498
|
}
|
|
262
499
|
|
|
263
500
|
private fun extractCacert(): String {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#include "CameraSmoother.hpp"
|
|
2
|
+
|
|
3
|
+
#include "EngineTunables.hpp"
|
|
4
|
+
|
|
5
|
+
#include <algorithm>
|
|
6
|
+
#include <cmath>
|
|
7
|
+
|
|
8
|
+
namespace reactnativecesium {
|
|
9
|
+
|
|
10
|
+
namespace {
|
|
11
|
+
|
|
12
|
+
inline double lerpAngleDeg(double a, double b, double t) {
|
|
13
|
+
// Wraps difference to [-180, 180] before applying the mix factor so we
|
|
14
|
+
// always rotate the short way around the unit circle.
|
|
15
|
+
double diff = std::fmod(b - a + 540.0, 360.0) - 180.0;
|
|
16
|
+
return a + diff * t;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
inline double angleDeltaAbsDeg(double a, double b) {
|
|
20
|
+
return std::abs(std::fmod(b - a + 540.0, 360.0) - 180.0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
inline double quatDotAbs(const glm::dquat& a, const glm::dquat& b) {
|
|
24
|
+
return std::abs(glm::dot(glm::normalize(a), glm::normalize(b)));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
inline double wrapLonDeltaDeg(double a, double b) {
|
|
28
|
+
return std::fmod(b - a + 540.0, 360.0) - 180.0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
} // namespace
|
|
32
|
+
|
|
33
|
+
CameraParams CameraSmoother::step(
|
|
34
|
+
const CameraParams& current, const CameraParams& target, double dt,
|
|
35
|
+
double recentIntervalSec) {
|
|
36
|
+
CameraParams next = current;
|
|
37
|
+
|
|
38
|
+
// ── lat/lon: adaptive exponential smoothing ─────────────────────────────
|
|
39
|
+
// τ scales with the cadence of incoming setCamera calls so gestures feel
|
|
40
|
+
// instant (τ at the floor) while sparse GPS-rate updates ease across each
|
|
41
|
+
// interval (τ near the ceiling).
|
|
42
|
+
const double tau = std::clamp(
|
|
43
|
+
tunables::kSmoothPositionAlpha * recentIntervalSec,
|
|
44
|
+
tunables::kSmoothPositionTauMin,
|
|
45
|
+
tunables::kSmoothPositionTauMax);
|
|
46
|
+
const double aPos = 1.0 - std::exp(-dt / tau);
|
|
47
|
+
|
|
48
|
+
const double dLat = target.latitude - current.latitude;
|
|
49
|
+
const double dLon = wrapLonDeltaDeg(current.longitude, target.longitude);
|
|
50
|
+
const bool snapThrough =
|
|
51
|
+
std::abs(dLat) > tunables::kPosSnapThroughDeg ||
|
|
52
|
+
std::abs(dLon) > tunables::kPosSnapThroughDeg;
|
|
53
|
+
|
|
54
|
+
if (snapThrough) {
|
|
55
|
+
next.latitude = target.latitude;
|
|
56
|
+
next.longitude = target.longitude;
|
|
57
|
+
} else {
|
|
58
|
+
next.latitude = current.latitude + aPos * dLat;
|
|
59
|
+
next.longitude = lerpAngleDeg(current.longitude, target.longitude, aPos);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const double aAlt = 1.0 - std::exp(-tunables::kSmoothAltitude * dt);
|
|
63
|
+
const double aHdg = 1.0 - std::exp(-tunables::kSmoothHeading * dt);
|
|
64
|
+
const double aPitch = 1.0 - std::exp(-tunables::kSmoothPitch * dt);
|
|
65
|
+
const double aRoll = 1.0 - std::exp(-tunables::kSmoothRoll * dt);
|
|
66
|
+
const double aViewQ = 1.0 - std::exp(-tunables::kSmoothViewCorr * dt);
|
|
67
|
+
|
|
68
|
+
next.altitude = current.altitude + aAlt * (target.altitude - current.altitude);
|
|
69
|
+
next.heading = lerpAngleDeg(current.heading, target.heading, aHdg);
|
|
70
|
+
next.pitch = lerpAngleDeg(current.pitch, target.pitch, aPitch);
|
|
71
|
+
next.roll = lerpAngleDeg(current.roll, target.roll, aRoll);
|
|
72
|
+
|
|
73
|
+
glm::dquat cq = glm::normalize(current.viewCorrection);
|
|
74
|
+
glm::dquat tq = glm::normalize(target.viewCorrection);
|
|
75
|
+
if (glm::dot(cq, tq) < 0.0) tq = -tq;
|
|
76
|
+
next.viewCorrection = glm::normalize(glm::slerp(cq, tq, aViewQ));
|
|
77
|
+
|
|
78
|
+
// Eventually-exact convergence: snap once the residual is sub-epsilon.
|
|
79
|
+
if (std::abs(target.latitude - next.latitude) <= tunables::kEpsLatLon)
|
|
80
|
+
next.latitude = target.latitude;
|
|
81
|
+
if (std::abs(wrapLonDeltaDeg(next.longitude, target.longitude))
|
|
82
|
+
<= tunables::kEpsLatLon)
|
|
83
|
+
next.longitude = target.longitude;
|
|
84
|
+
if (std::abs(target.altitude - next.altitude) <= tunables::kEpsAltitudeMeters)
|
|
85
|
+
next.altitude = target.altitude;
|
|
86
|
+
if (angleDeltaAbsDeg(next.heading, target.heading) <= tunables::kEpsAngleDeg)
|
|
87
|
+
next.heading = target.heading;
|
|
88
|
+
if (angleDeltaAbsDeg(next.pitch, target.pitch) <= tunables::kEpsAngleDeg)
|
|
89
|
+
next.pitch = target.pitch;
|
|
90
|
+
if (angleDeltaAbsDeg(next.roll, target.roll) <= tunables::kEpsAngleDeg)
|
|
91
|
+
next.roll = target.roll;
|
|
92
|
+
if ((1.0 - quatDotAbs(next.viewCorrection, target.viewCorrection))
|
|
93
|
+
<= tunables::kEpsQuatDot)
|
|
94
|
+
next.viewCorrection = target.viewCorrection;
|
|
95
|
+
|
|
96
|
+
return next;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
} // namespace reactnativecesium
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "GlobeCamera.hpp"
|
|
4
|
+
|
|
5
|
+
namespace reactnativecesium {
|
|
6
|
+
|
|
7
|
+
// Stateless helper. Given a current camera state and a target camera state,
|
|
8
|
+
// returns the next camera state for a frame of duration `dt` seconds.
|
|
9
|
+
//
|
|
10
|
+
// Per-DoF smoothing rules:
|
|
11
|
+
// - lat / lon: adaptive exponential smoothing `1 - exp(-dt/τ)` where
|
|
12
|
+
// τ = clamp(α · recentIntervalSec, τmin, τmax). High update rates (60 Hz
|
|
13
|
+
// gestures) drive τ to the floor so the camera tracks the finger without
|
|
14
|
+
// perceptible lag; sparse updates (1 Hz GPS) widen τ so each sample
|
|
15
|
+
// interpolates smoothly across its interval. Snap-through guard for
|
|
16
|
+
// genuine teleports keeps cross-planet `setCamera` jumps instant.
|
|
17
|
+
// - altitude / heading / pitch / roll / viewCorrection: exponential decay
|
|
18
|
+
// `1 - exp(-k * dt)` per DoF with k taken from EngineTunables.
|
|
19
|
+
// - When the delta drops below the matching epsilon, the value is snapped
|
|
20
|
+
// to the target so we eventually reach exact equality.
|
|
21
|
+
class CameraSmoother {
|
|
22
|
+
public:
|
|
23
|
+
static CameraParams step(const CameraParams& current,
|
|
24
|
+
const CameraParams& target,
|
|
25
|
+
double dt,
|
|
26
|
+
double recentIntervalSec);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
} // namespace reactnativecesium
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#include "CameraTargetState.hpp"
|
|
2
|
+
|
|
3
|
+
#include <cmath>
|
|
4
|
+
|
|
5
|
+
namespace reactnativecesium {
|
|
6
|
+
|
|
7
|
+
namespace {
|
|
8
|
+
inline double angleDeltaAbsDeg(double a, double b) {
|
|
9
|
+
return std::abs(std::fmod(b - a + 540.0, 360.0) - 180.0);
|
|
10
|
+
}
|
|
11
|
+
inline double quatDotAbs(const glm::dquat& a, const glm::dquat& b) {
|
|
12
|
+
return std::abs(glm::dot(glm::normalize(a), glm::normalize(b)));
|
|
13
|
+
}
|
|
14
|
+
} // namespace
|
|
15
|
+
|
|
16
|
+
bool CameraTargetState::deltaExceedsEpsilon(
|
|
17
|
+
const CameraParams& a, const CameraParams& b) {
|
|
18
|
+
return std::abs(a.latitude - b.latitude) > tunables::kEpsLatLon
|
|
19
|
+
|| std::abs(a.longitude - b.longitude) > tunables::kEpsLatLon
|
|
20
|
+
|| std::abs(a.altitude - b.altitude) > tunables::kEpsAltitudeMeters
|
|
21
|
+
|| angleDeltaAbsDeg(a.heading, b.heading) > tunables::kEpsAngleDeg
|
|
22
|
+
|| angleDeltaAbsDeg(a.pitch, b.pitch) > tunables::kEpsAngleDeg
|
|
23
|
+
|| angleDeltaAbsDeg(a.roll, b.roll) > tunables::kEpsAngleDeg
|
|
24
|
+
|| (1.0 - quatDotAbs(a.viewCorrection, b.viewCorrection))
|
|
25
|
+
> tunables::kEpsQuatDot;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
void CameraTargetState::noteUpdateCadence() noexcept {
|
|
29
|
+
const auto now = std::chrono::steady_clock::now();
|
|
30
|
+
if (!hasLastUpdate_) {
|
|
31
|
+
// Seed the EWMA with τmax so the first sample after a quiet period uses
|
|
32
|
+
// the full smoothing budget rather than the floor (5 ms ≈ snap).
|
|
33
|
+
ewmaIntervalSec_.store(tunables::kSmoothPositionTauMax / tunables::kSmoothPositionAlpha,
|
|
34
|
+
std::memory_order_release);
|
|
35
|
+
lastUpdateTime_ = now;
|
|
36
|
+
hasLastUpdate_ = true;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const double dt = std::chrono::duration<double>(now - lastUpdateTime_).count();
|
|
41
|
+
lastUpdateTime_ = now;
|
|
42
|
+
if (dt <= 0.0) return; // back-to-back writes from the same frame: ignore.
|
|
43
|
+
|
|
44
|
+
const double prev = ewmaIntervalSec_.load(std::memory_order_relaxed);
|
|
45
|
+
const double next = prev + tunables::kEwmaIntervalAlpha * (dt - prev);
|
|
46
|
+
ewmaIntervalSec_.store(next, std::memory_order_release);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
} // namespace reactnativecesium
|