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.
Files changed (59) hide show
  1. package/README.md +64 -11
  2. package/ReactNativeCesium.podspec +1 -1
  3. package/android/CMakeLists.txt +5 -3
  4. package/android/src/main/cpp/CesiumBridgeAndroid.cpp +197 -227
  5. package/android/src/main/cpp/CesiumBridgeAndroid.h +48 -49
  6. package/android/src/main/cpp/cpp-adapter.cpp +2 -2
  7. package/android/src/main/cpp/vulkan/VulkanBackend.cpp +1010 -569
  8. package/android/src/main/cpp/vulkan/VulkanBackend.h +121 -12
  9. package/android/src/main/cpp/vulkan/shaders/sky.frag +20 -10
  10. package/android/src/main/java/com/margelo/nitro/reactnativecesium/CesiumBridgeJNI.kt +28 -0
  11. package/android/src/main/java/com/margelo/nitro/reactnativecesium/HybridCesiumView.kt +295 -58
  12. package/cpp/engine/CameraSmoother.cpp +99 -0
  13. package/cpp/engine/CameraSmoother.hpp +29 -0
  14. package/cpp/engine/CameraTargetState.cpp +49 -0
  15. package/cpp/engine/CameraTargetState.hpp +121 -0
  16. package/cpp/engine/CesiumEngine.cpp +206 -60
  17. package/cpp/engine/CesiumEngine.hpp +77 -10
  18. package/cpp/engine/EngineTunables.hpp +79 -0
  19. package/cpp/engine/GeoidConverter.cpp +36 -0
  20. package/cpp/engine/GeoidConverter.hpp +10 -0
  21. package/cpp/engine/GlobeCamera.cpp +7 -2
  22. package/cpp/engine/MetricsAggregator.cpp +71 -0
  23. package/cpp/engine/MetricsAggregator.hpp +54 -0
  24. package/cpp/engine/TileLifecycleManager.cpp +0 -12
  25. package/cpp/engine/TileLifecycleManager.hpp +0 -3
  26. package/cpp/third_party/egm96/EGM96.c +289 -0
  27. package/cpp/third_party/egm96/EGM96.h +45 -0
  28. package/cpp/third_party/egm96/EGM96_data.h +65372 -0
  29. package/ios/CesiumBridge.h +24 -5
  30. package/ios/CesiumBridge.mm +149 -271
  31. package/ios/HybridCesiumView.swift +197 -40
  32. package/ios/metal/MetalBackend.mm +72 -23
  33. package/lib/typescript/src/specs/cesium-view.nitro.d.ts +26 -0
  34. package/lib/typescript/src/specs/cesium-view.nitro.d.ts.map +1 -1
  35. package/nitrogen/generated/android/c++/JCesiumMetrics.hpp +5 -1
  36. package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.cpp +109 -1
  37. package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.hpp +24 -0
  38. package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.cpp +48 -0
  39. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/CameraState.kt +25 -0
  40. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/CesiumMetrics.kt +32 -2
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/HybridCesiumViewSpec.kt +72 -0
  42. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/Quaternion.kt +19 -0
  43. package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Bridge.hpp +30 -0
  44. package/nitrogen/generated/ios/c++/HybridCesiumViewSpecSwift.hpp +85 -1
  45. package/nitrogen/generated/ios/c++/views/HybridCesiumViewComponent.mm +60 -0
  46. package/nitrogen/generated/ios/swift/CesiumMetrics.swift +7 -2
  47. package/nitrogen/generated/ios/swift/HybridCesiumViewSpec.swift +12 -0
  48. package/nitrogen/generated/ios/swift/HybridCesiumViewSpec_cxx.swift +288 -0
  49. package/nitrogen/generated/shared/c++/CesiumMetrics.hpp +5 -1
  50. package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.cpp +24 -0
  51. package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.hpp +25 -1
  52. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.cpp +132 -0
  53. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.hpp +13 -1
  54. package/nitrogen/generated/shared/json/CesiumViewConfig.json +12 -0
  55. package/package.json +4 -4
  56. package/scripts/cesium/clone.mjs +1 -1
  57. package/src/specs/cesium-view.nitro.ts +29 -0
  58. package/cpp/engine/DeletionQueue.cpp +0 -28
  59. package/cpp/engine/DeletionQueue.hpp +0 -28
@@ -8,15 +8,23 @@ class HybridCesiumView: HybridCesiumViewSpec {
8
8
  // MARK: - Props
9
9
 
10
10
  var ionAccessToken: String = "" {
11
- didSet { bridge?.updateIonAccessToken(ionAccessToken, assetId: Int64(ionAssetId)) }
11
+ didSet {
12
+ let token = ionAccessToken
13
+ let assetId = Int64(ionAssetId)
14
+ postToRender { [weak self] in self?.bridge?.updateIonAccessToken(token, assetId: assetId) }
15
+ }
12
16
  }
13
17
  var ionAssetId: Double = 1 {
14
- didSet { bridge?.updateIonAccessToken(ionAccessToken, assetId: Int64(ionAssetId)) }
18
+ didSet {
19
+ let token = ionAccessToken
20
+ let assetId = Int64(ionAssetId)
21
+ postToRender { [weak self] in self?.bridge?.updateIonAccessToken(token, assetId: assetId) }
22
+ }
15
23
  }
16
24
 
17
- /// Construction-time seed camera. Once the bridge exists, use `setCamera(_:)`
18
- /// for runtime updates; later prop writes are stored but do not drive the
19
- /// active native camera.
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.
20
28
  var initialCamera: CameraState = CameraState(
21
29
  latitude: 46.15,
22
30
  longitude: 7.35,
@@ -28,32 +36,82 @@ class HybridCesiumView: HybridCesiumViewSpec {
28
36
  )
29
37
  var pauseRendering: Bool = false {
30
38
  didSet {
31
- displayLink?.isPaused = pauseRendering
39
+ displayLink?.isPaused = pauseRendering // UIKit-ish, must stay on main
32
40
  if !pauseRendering {
33
- bridge?.markNeedsRender()
41
+ postToRender { [weak self] in self?.bridge?.markNeedsRender() }
34
42
  }
35
43
  }
36
44
  }
37
45
 
38
46
  var maximumScreenSpaceError: Double = 32 {
39
- didSet { bridge?.setMaximumScreenSpaceError(maximumScreenSpaceError) }
47
+ didSet { let v = maximumScreenSpaceError; postToRender { [weak self] in self?.bridge?.setMaximumScreenSpaceError(v) } }
40
48
  }
41
49
  var maximumSimultaneousTileLoads: Double = 12 {
42
- didSet { bridge?.setMaximumSimultaneousTileLoads(Int32(maximumSimultaneousTileLoads)) }
50
+ didSet { let v = Int32(maximumSimultaneousTileLoads); postToRender { [weak self] in self?.bridge?.setMaximumSimultaneousTileLoads(v) } }
43
51
  }
44
52
  var loadingDescendantLimit: Double = 20 {
45
- didSet { bridge?.setLoadingDescendantLimit(Int32(loadingDescendantLimit)) }
53
+ didSet { let v = Int32(loadingDescendantLimit); postToRender { [weak self] in self?.bridge?.setLoadingDescendantLimit(v) } }
46
54
  }
47
55
  var msaaSampleCount: Double = 1 {
48
56
  didSet {
49
57
  let s = Int32(msaaSampleCount)
50
- bridge?.setMsaaSampleCount(s)
58
+ // applyMtkViewMsaa touches MTKView/UIKit so it stays on main.
51
59
  applyMtkViewMsaa(Int(s))
60
+ postToRender { [weak self] in self?.bridge?.setMsaaSampleCount(s) }
52
61
  }
53
62
  }
54
63
  var ionImageryAssetId: Double = 1 {
55
- didSet { bridge?.updateImageryAssetId(Int64(ionImageryAssetId)) }
64
+ didSet { let v = Int64(ionImageryAssetId); postToRender { [weak self] in self?.bridge?.updateImageryAssetId(v) } }
65
+ }
66
+
67
+ // ── Optional perf / quality knobs ────────────────────────────────────────
68
+ var maximumCachedMiB: Double? {
69
+ didSet { if let v = maximumCachedMiB { postToRender { [weak self] in self?.bridge?.setMaximumCachedMiB(Int32(v)) } } }
70
+ }
71
+ var preloadAncestors: Bool? {
72
+ didSet { if let v = preloadAncestors { postToRender { [weak self] in self?.bridge?.setPreloadAncestors(v) } } }
73
+ }
74
+ var preloadSiblings: Bool? {
75
+ didSet { if let v = preloadSiblings { postToRender { [weak self] in self?.bridge?.setPreloadSiblings(v) } } }
76
+ }
77
+ var forbidHoles: Bool? {
78
+ didSet { if let v = forbidHoles { postToRender { [weak self] in self?.bridge?.setForbidHoles(v) } } }
79
+ }
80
+ var enableWaterMask: Bool? {
81
+ didSet { if let v = enableWaterMask { postToRender { [weak self] in self?.bridge?.setEnableWaterMask(v) } } }
82
+ }
83
+ var enableFogCulling: Bool? {
84
+ didSet { if let v = enableFogCulling { postToRender { [weak self] in self?.bridge?.setEnableFogCulling(v) } } }
85
+ }
86
+ var enforceCulledScreenSpaceError: Bool? {
87
+ didSet { if let v = enforceCulledScreenSpaceError { postToRender { [weak self] in self?.bridge?.setEnforceCulledScreenSpaceError(v) } } }
56
88
  }
89
+ var culledScreenSpaceError: Double? {
90
+ didSet { if let v = culledScreenSpaceError { postToRender { [weak self] in self?.bridge?.setCulledScreenSpaceError(v) } } }
91
+ }
92
+ var enableLodTransitionPeriod: Bool? {
93
+ didSet { if let v = enableLodTransitionPeriod { postToRender { [weak self] in self?.bridge?.setEnableLodTransitionPeriod(v) } } }
94
+ }
95
+ var lodTransitionLength: Double? {
96
+ didSet { if let v = lodTransitionLength { postToRender { [weak self] in self?.bridge?.setLodTransitionLength(v) } } }
97
+ }
98
+ var sqliteCacheMaxRows: Double? {
99
+ didSet { if let v = sqliteCacheMaxRows { postToRender { [weak self] in self?.bridge?.setSqliteCacheMaxRows(Int32(v)) } } }
100
+ }
101
+ var taskProcessorThreads: Double? {
102
+ didSet { if let v = taskProcessorThreads { postToRender { [weak self] in self?.bridge?.setTaskProcessorThreads(Int32(v)) } } }
103
+ }
104
+
105
+ // MARK: - Render-thread dispatch helper
106
+
107
+ /// Posts a closure to the dedicated render queue. Used by every prop setter
108
+ /// that mutates engine state, so the EngineConfig + GPU resources are only
109
+ /// ever touched from one thread (renderQueue) — the render path itself, the
110
+ /// surface init, and bridge teardown all run there.
111
+ private func postToRender(_ action: @escaping () -> Void) {
112
+ renderQueue.async { action() }
113
+ }
114
+
57
115
  var onMetrics: ((CesiumMetrics) -> Void)?
58
116
 
59
117
  // MARK: - Methods
@@ -81,15 +139,19 @@ class HybridCesiumView: HybridCesiumViewSpec {
81
139
  }
82
140
 
83
141
  func setCamera(camera: CameraState) throws {
84
- // Runtime camera updates flow through this method; if the view is not ready
85
- // yet we keep the latest value and apply it during initialization.
86
- initialCamera = camera
87
- pushCameraStateIfChanged(camera)
142
+ // Runtime camera updates flow through this method. We do *not* mutate
143
+ // `initialCamera` (it is construction-time only). If the view is not
144
+ // ready yet, the value is buffered in `pendingRuntimeCamera` and pushed
145
+ // during initialization.
146
+ pendingRuntimeCamera = camera
147
+ pendingRuntimeViewCorrection = nil
148
+ postToRender { [weak self] in self?.pushCameraStateIfChanged(camera) }
88
149
  }
89
150
 
90
151
  func setCameraQuaternion(camera: CameraState, viewCorrection: Quaternion) throws {
91
- initialCamera = camera
92
- pushCameraQuaternionIfChanged(camera, viewCorrection: viewCorrection)
152
+ pendingRuntimeCamera = camera
153
+ pendingRuntimeViewCorrection = viewCorrection
154
+ postToRender { [weak self] in self?.pushCameraQuaternionIfChanged(camera, viewCorrection: viewCorrection) }
93
155
  }
94
156
 
95
157
  func getViewCorrection() throws -> Promise<Quaternion> {
@@ -120,8 +182,28 @@ class HybridCesiumView: HybridCesiumViewSpec {
120
182
  private var lastPushedCameraState: CameraState?
121
183
  /// Last quaternion pushed via `setCameraQuaternion`; `nil` if only `setCamera` has been used.
122
184
  private var lastPushedViewCorrection: Quaternion?
185
+ /// Camera state requested by `setCamera` before the bridge was ready.
186
+ private var pendingRuntimeCamera: CameraState?
187
+ private var pendingRuntimeViewCorrection: Quaternion?
123
188
  private var lastBridgePixelSize: CGSize = .zero
124
189
 
190
+ /// Dedicated serial render queue. CADisplayLink fires on `.main`, but the
191
+ /// actual `bridge.shouldRenderNextFrame()` / `bridge.renderFrame(...)` work
192
+ /// is dispatched here so the main thread is never blocked on Cesium tile
193
+ /// loading or Metal command-buffer encoding. All bridge prop setters also
194
+ /// dispatch here so they serialize with rendering and never race with
195
+ /// engine state.
196
+ private let renderQueue = DispatchQueue(
197
+ label: "com.margelo.cesium.render",
198
+ qos: .userInteractive
199
+ )
200
+ /// Drops a CADisplayLink tick if the render queue is still finishing the
201
+ /// previous frame — keeps it from queuing up an unbounded backlog under
202
+ /// stutter (Metal's frameSemaphore would already throttle, but this saves
203
+ /// the dispatch hop and keeps `dt` accurate).
204
+ private var renderInFlight = false
205
+ private let renderInFlightLock = NSLock()
206
+
125
207
  var view: UIView { metalView }
126
208
 
127
209
  override init() {
@@ -185,7 +267,17 @@ class HybridCesiumView: HybridCesiumViewSpec {
185
267
  if !ionAccessToken.isEmpty {
186
268
  bridge?.updateIonAccessToken(ionAccessToken, assetId: Int64(ionAssetId))
187
269
  }
270
+ // Apply construction-time seed first; if the consumer already invoked
271
+ // setCamera / setCameraQuaternion before we were ready, replay it last so
272
+ // the runtime value wins.
188
273
  pushCameraStateIfChanged(initialCamera)
274
+ if let runtime = pendingRuntimeCamera {
275
+ if let q = pendingRuntimeViewCorrection {
276
+ pushCameraQuaternionIfChanged(runtime, viewCorrection: q)
277
+ } else {
278
+ pushCameraStateIfChanged(runtime)
279
+ }
280
+ }
189
281
  if ionImageryAssetId != 1 {
190
282
  bridge?.updateImageryAssetId(Int64(ionImageryAssetId))
191
283
  }
@@ -215,6 +307,18 @@ class HybridCesiumView: HybridCesiumViewSpec {
215
307
  bridge?.setMaximumScreenSpaceError(maximumScreenSpaceError)
216
308
  bridge?.setMaximumSimultaneousTileLoads(Int32(maximumSimultaneousTileLoads))
217
309
  bridge?.setLoadingDescendantLimit(Int32(loadingDescendantLimit))
310
+ if let v = maximumCachedMiB { bridge?.setMaximumCachedMiB(Int32(v)) }
311
+ if let v = preloadAncestors { bridge?.setPreloadAncestors(v) }
312
+ if let v = preloadSiblings { bridge?.setPreloadSiblings(v) }
313
+ if let v = forbidHoles { bridge?.setForbidHoles(v) }
314
+ if let v = enableWaterMask { bridge?.setEnableWaterMask(v) }
315
+ if let v = enableFogCulling { bridge?.setEnableFogCulling(v) }
316
+ if let v = enforceCulledScreenSpaceError { bridge?.setEnforceCulledScreenSpaceError(v) }
317
+ if let v = culledScreenSpaceError { bridge?.setCulledScreenSpaceError(v) }
318
+ if let v = enableLodTransitionPeriod { bridge?.setEnableLodTransitionPeriod(v) }
319
+ if let v = lodTransitionLength { bridge?.setLodTransitionLength(v) }
320
+ if let v = sqliteCacheMaxRows { bridge?.setSqliteCacheMaxRows(Int32(v)) }
321
+ if let v = taskProcessorThreads { bridge?.setTaskProcessorThreads(Int32(v)) }
218
322
  }
219
323
 
220
324
  /// Pushes camera state into native bridge only when values changed.
@@ -302,54 +406,107 @@ class HybridCesiumView: HybridCesiumViewSpec {
302
406
 
303
407
  let dt = max(dl.targetTimestamp - dl.timestamp, 1.0 / 120.0)
304
408
 
409
+ // metalView.bounds / drawableSize are UIKit, must stay on main.
305
410
  let scale = metalView.contentScaleFactor
306
411
  let w = Int(metalView.bounds.width * scale)
307
412
  let h = Int(metalView.bounds.height * scale)
308
-
413
+ var didResize = false
309
414
  if w > 0 && h > 0 {
310
415
  let pixelSize = CGSize(width: w, height: h)
311
416
  if lastBridgePixelSize != pixelSize {
312
417
  metalView.drawableSize = pixelSize
313
418
  lastBridgePixelSize = pixelSize
314
- bridge.resize(Int32(w), height: Int32(h))
315
- bridge.markNeedsRender()
419
+ didResize = true
316
420
  }
317
421
  }
318
422
 
319
- let shouldRenderNow = bridge.shouldRenderNextFrame()
320
- if shouldRenderNow {
321
- setDisplayLinkFrameRate(idle: false)
322
- idleProbeAccumulator = 0
323
- bridge.renderFrame(withDt: dt)
324
- } else {
325
- setDisplayLinkFrameRate(idle: true)
326
- idleProbeAccumulator += dt
327
- if idleProbeAccumulator >= 0.25 {
328
- // Safety probe: occasionally tick engine state for late tile completions.
329
- idleProbeAccumulator = 0
423
+ // Throttle: drop the tick if the render queue hasn't finished the previous
424
+ // frame yet. This keeps the dispatch backlog at zero and lets the next
425
+ // CADisplayLink fire pick up a fresh dt.
426
+ renderInFlightLock.lock()
427
+ if renderInFlight {
428
+ renderInFlightLock.unlock()
429
+ return
430
+ }
431
+ renderInFlight = true
432
+ renderInFlightLock.unlock()
433
+
434
+ let resizedW = w
435
+ let resizedH = h
436
+ let weakSelf = self // captured for main-thread post-frame work
437
+ renderQueue.async { [weak self, weak bridge] in
438
+ guard let self = self, let bridge = bridge else {
439
+ weakSelf.renderInFlightLock.lock()
440
+ weakSelf.renderInFlight = false
441
+ weakSelf.renderInFlightLock.unlock()
442
+ return
443
+ }
444
+
445
+ if didResize {
446
+ bridge.resize(Int32(resizedW), height: Int32(resizedH))
330
447
  bridge.markNeedsRender()
331
448
  }
332
- }
333
449
 
334
- metricsFrameCounter += 1
335
- if metricsFrameCounter >= 20, let cb = onMetrics {
336
- metricsFrameCounter = 0
337
- let m = CesiumMetrics(
450
+ let shouldRenderNow = bridge.shouldRenderNextFrame()
451
+ let nextIsIdle: Bool
452
+ if shouldRenderNow {
453
+ self.idleProbeAccumulator = 0
454
+ bridge.renderFrame(withDt: dt)
455
+ nextIsIdle = false
456
+ } else {
457
+ self.idleProbeAccumulator += dt
458
+ if self.idleProbeAccumulator >= 0.25 {
459
+ // Safety probe: occasionally tick engine state for late tile completions.
460
+ self.idleProbeAccumulator = 0
461
+ bridge.markNeedsRender()
462
+ }
463
+ nextIsIdle = true
464
+ }
465
+
466
+ self.metricsFrameCounter += 1
467
+ let shouldEmitMetrics =
468
+ self.metricsFrameCounter >= 20 && self.onMetrics != nil
469
+ let metricsSnapshot: CesiumMetrics? = shouldEmitMetrics ? CesiumMetrics(
338
470
  fps: bridge.metricsFps,
339
471
  tilesRendered: Double(bridge.metricsTilesRendered),
340
472
  tilesLoading: Double(bridge.metricsTilesLoading),
341
473
  tilesVisited: Double(bridge.metricsTilesVisited),
342
474
  ionTokenConfigured: bridge.metricsIonTokenConfigured,
475
+ tlsConfigured: bridge.metricsTlsConfigured,
343
476
  tilesetReady: bridge.metricsTilesetReady,
344
477
  creditsPlainText: bridge.metricsCreditsPlainText
345
- )
346
- cb(m)
478
+ ) : nil
479
+ if shouldEmitMetrics { self.metricsFrameCounter = 0 }
480
+
481
+ // Hop back to main to update CADisplayLink rate (UIKit) and emit the
482
+ // metrics callback (consumer may touch UI from it).
483
+ DispatchQueue.main.async { [weak self] in
484
+ guard let self = self else { return }
485
+ self.setDisplayLinkFrameRate(idle: nextIsIdle)
486
+ if let m = metricsSnapshot, let cb = self.onMetrics {
487
+ cb(m)
488
+ }
489
+ }
490
+
491
+ self.renderInFlightLock.lock()
492
+ self.renderInFlight = false
493
+ self.renderInFlightLock.unlock()
347
494
  }
348
495
  }
349
496
 
350
497
  deinit {
351
498
  layoutPollTimer?.invalidate()
352
499
  displayLink?.invalidate()
353
- bridge?.shutdown()
500
+
501
+ // Drain the render queue so we don't release `bridge` while it's
502
+ // mid-encode. `sync` here is bounded by Metal's frameSemaphore wait —
503
+ // which itself is already bounded to 1 s in MetalBackend::shutdown().
504
+ let bridgeRef = bridge
505
+ bridge = nil
506
+ if let b = bridgeRef {
507
+ renderQueue.sync {
508
+ b.shutdown()
509
+ }
510
+ }
354
511
  }
355
512
  }
@@ -304,15 +304,19 @@ fragment float4 skyFragment(SkyVOut in [[stage_in]],
304
304
  float3 org=u.cameraEcef.xyz;
305
305
  float2 at=rsi(org,rd,AR);
306
306
  if(at.y<0)return float4(0,0,0,1);
307
- float2 er=rsi(org,rd,ER);
308
- float tmax=at.y;if(er.x>0)tmax=min(tmax,er.x);
307
+ // No earth-surface tmax clamp — that creates a visible band discontinuity
308
+ // at the geometric horizon. Integrate full atmospheric path and clamp
309
+ // underground sample heights to zero (ground-level density).
310
+ float tmax=at.y;
309
311
  float tmin=max(at.x,0.f);if(tmin>=tmax)return float4(0,0,0,1);
310
- const int S=16,L=4;
312
+ // Halved from 16/4 to 8/2 — visually negligible at typical phone DPI
313
+ // (atmospheric scattering banding is sub-pixel at this sample count).
314
+ const int S=8,L=2;
311
315
  float ss=(tmax-tmin)/float(S);
312
316
  float3 rayl=0,miel=0;float oR=0,oM=0;
313
317
  for(int i=0;i<S;++i){
314
318
  float t=tmin+(float(i)+.5f)*ss;
315
- float3 sp=org+rd*t;float h=length(sp)-ER;
319
+ float3 sp=org+rd*t;float h=max(0.f,length(sp)-ER);
316
320
  float hr=exp(-h/10000.f)*ss,hm=exp(-h/3200.f)*ss;
317
321
  oR+=hr;oM+=hm;
318
322
  float2 lh=rsi(sp,u.lightDir.xyz,AR);
@@ -330,11 +334,16 @@ fragment float4 skyFragment(SkyVOut in [[stage_in]],
330
334
  float rPh=.75f*(1+cos2);
331
335
  float g2=MG*MG,mPh=1.5f*((1-g2)/(2+g2))*(1+cos2)/pow(1+g2-2*MG*cosT,1.5f);
332
336
  float3 col=LINT*(rayl*RAY*rPh+miel*MIE3*mPh);
333
- col=1-exp(-col*1.1f);col*=float3(.18f,.45f,1.f);
334
- float3 nu=normalize(org);float up=saturate(dot(rd,nu));
335
- float camH=max(0.f,length(org)-ER);
336
- float hStr=saturate(1-camH/35000.f);
337
- col=mix(col,float3(.40f,.62f,.96f),(1-smoothstep(.05f,.45f,up))*.40f*hStr);
337
+ col=1-exp(-col*1.1f);
338
+ col*=float3(.30f,.55f,1.f);
339
+ // Single-scatter atmosphere produces sunset-like yellow at horizons with
340
+ // long path lengths (because blue out-scatters faster). Real daytime sky
341
+ // appears whitish-blue at horizon thanks to multiple scattering, which we
342
+ // approximate here by blending toward a natural blue based on the
343
+ // total atmospheric path length traversed by the view ray.
344
+ float pathLen=tmax-tmin;
345
+ float hazeFactor=smoothstep(100000.f,600000.f,pathLen);
346
+ col=mix(col,float3(.35f,.58f,.90f),hazeFactor*.65f);
338
347
  return float4(col,1.f);
339
348
  }
340
349
  )MSL";
@@ -409,8 +418,6 @@ void MetalBackend::buildRenderPipelines() {
409
418
  CAMetalLayer* layer = (__bridge CAMetalLayer*)metalLayer_;
410
419
  const MTLPixelFormat colorFmt = layer.pixelFormat;
411
420
 
412
- NSError* err = nil;
413
-
414
421
  if (terrainPipeline_) {
415
422
  CFRelease(terrainPipeline_);
416
423
  terrainPipeline_ = nullptr;
@@ -420,9 +427,16 @@ void MetalBackend::buildRenderPipelines() {
420
427
  skyPipeline_ = nullptr;
421
428
  }
422
429
 
430
+ // Use a separate NSError* per compile + a strict result check. Sharing a
431
+ // single `err` across multiple operations conflated unrelated failures
432
+ // (P2-error-handling).
433
+ NSError* terrainLibErr = nil;
423
434
  id<MTLLibrary> tLib = [dev newLibraryWithSource:kTerrainShaderSrc
424
- options:nil error:&err];
425
- if (err) NSLog(@"[MetalBackend] terrain shader error: %@", err);
435
+ options:nil error:&terrainLibErr];
436
+ if (!tLib) {
437
+ NSLog(@"[MetalBackend] terrain shader compilation FAILED: %@", terrainLibErr);
438
+ return;
439
+ }
426
440
 
427
441
  MTLRenderPipelineDescriptor* tDesc = [MTLRenderPipelineDescriptor new];
428
442
  tDesc.vertexFunction = [tLib newFunctionWithName:@"terrainVertex"];
@@ -430,13 +444,22 @@ void MetalBackend::buildRenderPipelines() {
430
444
  tDesc.colorAttachments[0].pixelFormat = colorFmt;
431
445
  tDesc.depthAttachmentPixelFormat = MTLPixelFormatDepth32Float;
432
446
  tDesc.rasterSampleCount = (NSUInteger)std::max(1, sampleCount_);
433
- terrainPipeline_ = (__bridge_retained void*)
434
- [dev newRenderPipelineStateWithDescriptor:tDesc error:&err];
435
- if (err) NSLog(@"[MetalBackend] terrain pipeline error: %@", err);
447
+ NSError* terrainPipelineErr = nil;
448
+ id<MTLRenderPipelineState> terrainPipeline =
449
+ [dev newRenderPipelineStateWithDescriptor:tDesc error:&terrainPipelineErr];
450
+ if (!terrainPipeline) {
451
+ NSLog(@"[MetalBackend] terrain pipeline build FAILED: %@", terrainPipelineErr);
452
+ return;
453
+ }
454
+ terrainPipeline_ = (__bridge_retained void*)terrainPipeline;
436
455
 
456
+ NSError* skyLibErr = nil;
437
457
  id<MTLLibrary> sLib = [dev newLibraryWithSource:kSkyShaderSrc
438
- options:nil error:&err];
439
- if (err) NSLog(@"[MetalBackend] sky shader error: %@", err);
458
+ options:nil error:&skyLibErr];
459
+ if (!sLib) {
460
+ NSLog(@"[MetalBackend] sky shader compilation FAILED: %@", skyLibErr);
461
+ return;
462
+ }
440
463
 
441
464
  MTLRenderPipelineDescriptor* sDesc = [MTLRenderPipelineDescriptor new];
442
465
  sDesc.vertexFunction = [sLib newFunctionWithName:@"skyVertex"];
@@ -444,9 +467,14 @@ void MetalBackend::buildRenderPipelines() {
444
467
  sDesc.colorAttachments[0].pixelFormat = colorFmt;
445
468
  sDesc.depthAttachmentPixelFormat = MTLPixelFormatDepth32Float;
446
469
  sDesc.rasterSampleCount = (NSUInteger)std::max(1, sampleCount_);
447
- skyPipeline_ = (__bridge_retained void*)
448
- [dev newRenderPipelineStateWithDescriptor:sDesc error:&err];
449
- if (err) NSLog(@"[MetalBackend] sky pipeline error: %@", err);
470
+ NSError* skyPipelineErr = nil;
471
+ id<MTLRenderPipelineState> skyPipeline =
472
+ [dev newRenderPipelineStateWithDescriptor:sDesc error:&skyPipelineErr];
473
+ if (!skyPipeline) {
474
+ NSLog(@"[MetalBackend] sky pipeline build FAILED: %@", skyPipelineErr);
475
+ return;
476
+ }
477
+ skyPipeline_ = (__bridge_retained void*)skyPipeline;
450
478
 
451
479
  if (!terrainDepthState_) {
452
480
  MTLDepthStencilDescriptor* dd = [MTLDepthStencilDescriptor new];
@@ -473,8 +501,20 @@ void MetalBackend::setMsaaSampleCount(int sc) {
473
501
 
474
502
  if (frameSemaphore_) {
475
503
  dispatch_semaphore_t sem = (__bridge dispatch_semaphore_t)frameSemaphore_;
504
+ // Bound the wait per slot at 1 second so a stuck GPU encode (the only
505
+ // realistic cause of a missed completion handler) cannot wedge the UI
506
+ // thread forever. On timeout we proceed regardless: the worst that can
507
+ // happen is one frame's worth of pipeline rebuild lands on a slot the
508
+ // GPU is still using, which Metal handles gracefully (the prior
509
+ // pipeline stays alive via ARC until the encoder finishes).
476
510
  for (int i = 0; i < kMaxFramesInFlight; ++i) {
477
- dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
511
+ dispatch_time_t deadline = dispatch_time(DISPATCH_TIME_NOW,
512
+ (int64_t)NSEC_PER_SEC);
513
+ if (dispatch_semaphore_wait(sem, deadline) != 0) {
514
+ NSLog(@"[CesiumMetal] MSAA-change semaphore timed out (slot %d); "
515
+ @"proceeding with rebuild", i);
516
+ break;
517
+ }
478
518
  }
479
519
  }
480
520
 
@@ -499,10 +539,19 @@ void MetalBackend::resize(int width, int height) {
499
539
  void MetalBackend::shutdown() {
500
540
  // Drain in-flight frames: wait until all GPU completion handlers have fired
501
541
  // before releasing the semaphore and the persistent buffers they reference.
542
+ // Bounded wait — if a slot doesn't release in 1 s we are almost certainly
543
+ // looking at a hung GPU command. Forcing release lets the app exit/teardown
544
+ // cleanly rather than ANRing on backgrounding.
502
545
  if (frameSemaphore_) {
503
546
  dispatch_semaphore_t sem = (__bridge dispatch_semaphore_t)frameSemaphore_;
504
547
  for (int i = 0; i < kMaxFramesInFlight; ++i) {
505
- dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
548
+ dispatch_time_t deadline = dispatch_time(DISPATCH_TIME_NOW,
549
+ (int64_t)NSEC_PER_SEC);
550
+ if (dispatch_semaphore_wait(sem, deadline) != 0) {
551
+ NSLog(@"[CesiumMetal] shutdown semaphore wait timed out at slot %d; "
552
+ @"force-releasing and continuing teardown", i);
553
+ break;
554
+ }
506
555
  }
507
556
  CFRelease(frameSemaphore_);
508
557
  frameSemaphore_ = nullptr;
@@ -26,6 +26,8 @@ export interface CesiumMetrics {
26
26
  tilesLoading: number;
27
27
  tilesVisited: number;
28
28
  ionTokenConfigured: boolean;
29
+ /** True when a CA bundle has been resolved for libcurl TLS. */
30
+ tlsConfigured: boolean;
29
31
  tilesetReady: boolean;
30
32
  creditsPlainText: string;
31
33
  }
@@ -41,6 +43,30 @@ export interface CesiumViewProps extends HybridViewProps {
41
43
  /** 1 = off; 2 or 4 = MSAA when supported. */
42
44
  msaaSampleCount: number;
43
45
  ionImageryAssetId: number;
46
+ /** RAM budget for live tileset, in mebibytes. Default 256. */
47
+ maximumCachedMiB?: number;
48
+ /** Pre-load ancestor tiles around the visible set (smoother panning, more loads). Default true. */
49
+ preloadAncestors?: boolean;
50
+ /** Pre-load sibling tiles around the visible set. Default true. */
51
+ preloadSiblings?: boolean;
52
+ /** Refuse to render holes in the terrain. Default true. Relax for fast pans. */
53
+ forbidHoles?: boolean;
54
+ /** Decode water-mask textures (coastline shading). Default true. */
55
+ enableWaterMask?: boolean;
56
+ /** Cull tiles fully inside the fog volume. Default false. */
57
+ enableFogCulling?: boolean;
58
+ /** When fog culling is on, override SSE for fogged tiles. Default true. */
59
+ enforceCulledScreenSpaceError?: boolean;
60
+ /** SSE applied to fogged tiles when `enforceCulledScreenSpaceError` is true. Default 64. */
61
+ culledScreenSpaceError?: number;
62
+ /** Cross-fade between LODs when refining (smoother pop-in). Default false. */
63
+ enableLodTransitionPeriod?: boolean;
64
+ /** Length (s) of the LOD cross-fade. Default 1. */
65
+ lodTransitionLength?: number;
66
+ /** Sqlite asset cache row budget. Default 4096. */
67
+ sqliteCacheMaxRows?: number;
68
+ /** Worker thread count. 0 = auto (hardware_concurrency-1, clamped 2..8). */
69
+ taskProcessorThreads?: number;
44
70
  onMetrics?: (metrics: CesiumMetrics) => void;
45
71
  }
46
72
  export interface CesiumViewMethods extends HybridViewMethods {
@@ -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,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;IACzB,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":"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;IAE7B,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"}
@@ -41,6 +41,8 @@ namespace margelo::nitro::reactnativecesium {
41
41
  double tilesVisited = this->getFieldValue(fieldTilesVisited);
42
42
  static const auto fieldIonTokenConfigured = clazz->getField<jboolean>("ionTokenConfigured");
43
43
  jboolean ionTokenConfigured = this->getFieldValue(fieldIonTokenConfigured);
44
+ static const auto fieldTlsConfigured = clazz->getField<jboolean>("tlsConfigured");
45
+ jboolean tlsConfigured = this->getFieldValue(fieldTlsConfigured);
44
46
  static const auto fieldTilesetReady = clazz->getField<jboolean>("tilesetReady");
45
47
  jboolean tilesetReady = this->getFieldValue(fieldTilesetReady);
46
48
  static const auto fieldCreditsPlainText = clazz->getField<jni::JString>("creditsPlainText");
@@ -51,6 +53,7 @@ namespace margelo::nitro::reactnativecesium {
51
53
  tilesLoading,
52
54
  tilesVisited,
53
55
  static_cast<bool>(ionTokenConfigured),
56
+ static_cast<bool>(tlsConfigured),
54
57
  static_cast<bool>(tilesetReady),
55
58
  creditsPlainText->toStdString()
56
59
  );
@@ -62,7 +65,7 @@ namespace margelo::nitro::reactnativecesium {
62
65
  */
63
66
  [[maybe_unused]]
64
67
  static jni::local_ref<JCesiumMetrics::javaobject> fromCpp(const CesiumMetrics& value) {
65
- using JSignature = JCesiumMetrics(double, double, double, double, jboolean, jboolean, jni::alias_ref<jni::JString>);
68
+ using JSignature = JCesiumMetrics(double, double, double, double, jboolean, jboolean, jboolean, jni::alias_ref<jni::JString>);
66
69
  static const auto clazz = javaClassStatic();
67
70
  static const auto create = clazz->getStaticMethod<JSignature>("fromCpp");
68
71
  return create(
@@ -72,6 +75,7 @@ namespace margelo::nitro::reactnativecesium {
72
75
  value.tilesLoading,
73
76
  value.tilesVisited,
74
77
  value.ionTokenConfigured,
78
+ value.tlsConfigured,
75
79
  value.tilesetReady,
76
80
  jni::make_jstring(value.creditsPlainText)
77
81
  );