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
@@ -0,0 +1,121 @@
1
+ #pragma once
2
+
3
+ #include "EngineTunables.hpp"
4
+ #include "GlobeCamera.hpp"
5
+
6
+ #include <atomic>
7
+ #include <chrono>
8
+ #include <mutex>
9
+
10
+ namespace reactnativecesium {
11
+
12
+ // Thread-safe holder for the *demand* camera (what the JS side last asked for).
13
+ //
14
+ // Writers: the React Native JS / worklet thread, via setCamera / setCameraQuaternion.
15
+ // Readers: the dedicated render thread (per frame), via shouldRenderNextFrame /
16
+ // renderFrame.
17
+ //
18
+ // We use a small mutex around `target_` because the writes are uncontended and
19
+ // the read side only happens once per frame; the cost is negligible compared to
20
+ // the cost of *not* synchronising a 56-byte struct that contains a glm::dquat.
21
+ //
22
+ // `dirty_` and `forceRender_` are independent atomic flags so the read side can
23
+ // poll without taking the lock when the camera has not moved.
24
+ class CameraTargetState {
25
+ public:
26
+ CameraTargetState() = default;
27
+
28
+ // Replace the entire target (does not touch viewCorrection if you want to
29
+ // preserve it — use setHpr instead).
30
+ void setAll(const CameraParams& target) {
31
+ {
32
+ std::lock_guard<std::mutex> g(mutex_);
33
+ target_ = target;
34
+ }
35
+ noteUpdateCadence();
36
+ dirty_.store(true, std::memory_order_release);
37
+ forceRender_.store(true, std::memory_order_release);
38
+ }
39
+
40
+ // Update lat/lon/alt + heading/pitch/roll, leaving viewCorrection unchanged.
41
+ void setHpr(double lat, double lon, double alt,
42
+ double heading, double pitch, double roll) {
43
+ {
44
+ std::lock_guard<std::mutex> g(mutex_);
45
+ target_.latitude = lat;
46
+ target_.longitude = lon;
47
+ target_.altitude = alt;
48
+ target_.heading = heading;
49
+ target_.pitch = pitch;
50
+ target_.roll = roll;
51
+ }
52
+ noteUpdateCadence();
53
+ dirty_.store(true, std::memory_order_release);
54
+ forceRender_.store(true, std::memory_order_release);
55
+ }
56
+
57
+ void setViewCorrection(const glm::dquat& q) {
58
+ {
59
+ std::lock_guard<std::mutex> g(mutex_);
60
+ target_.viewCorrection = q;
61
+ }
62
+ noteUpdateCadence();
63
+ dirty_.store(true, std::memory_order_release);
64
+ forceRender_.store(true, std::memory_order_release);
65
+ }
66
+
67
+ // Snapshot the demand target (read side; one mutex per frame).
68
+ CameraParams snapshot() const {
69
+ std::lock_guard<std::mutex> g(mutex_);
70
+ return target_;
71
+ }
72
+
73
+ // Lightweight, lock-free check used by the render thread to decide whether
74
+ // it should bother taking a full snapshot at all.
75
+ bool isDirty() const noexcept {
76
+ return dirty_.load(std::memory_order_acquire);
77
+ }
78
+ void clearDirty() noexcept {
79
+ dirty_.store(false, std::memory_order_release);
80
+ }
81
+
82
+ bool consumeForceRender() noexcept {
83
+ return forceRender_.exchange(false, std::memory_order_acq_rel);
84
+ }
85
+ void requestForceRender() noexcept {
86
+ forceRender_.store(true, std::memory_order_release);
87
+ }
88
+
89
+ // Compares snapshot to "current" (the engine's current camera) and returns
90
+ // true if any DoF differs by more than the matching epsilon. Used to decide
91
+ // whether to render this frame.
92
+ static bool deltaExceedsEpsilon(const CameraParams& a, const CameraParams& b);
93
+
94
+ // EWMA of the inter-arrival interval (seconds) of demand updates. Used by
95
+ // CameraSmoother to scale the lat/lon time constant: high update rate ⇒
96
+ // tiny τ (snappy gestures); low update rate ⇒ larger τ (smooth GPS catch-up).
97
+ // Lock-free read on the render-thread hot path.
98
+ double recentIntervalSec() const noexcept {
99
+ return ewmaIntervalSec_.load(std::memory_order_acquire);
100
+ }
101
+
102
+ private:
103
+ // Folds the time since the previous demand update into ewmaIntervalSec_.
104
+ // Called under (or after) the target_ mutex; the EWMA itself is atomic so
105
+ // the read side never blocks. Writes are uncontended (only JS-thread).
106
+ void noteUpdateCadence() noexcept;
107
+
108
+ mutable std::mutex mutex_;
109
+ CameraParams target_;
110
+ std::atomic<bool> dirty_{false};
111
+ std::atomic<bool> forceRender_{true};
112
+
113
+ // Cadence tracker. lastUpdateTime_ is only touched on the writer (JS)
114
+ // thread, so it does not need synchronisation; ewmaIntervalSec_ is read
115
+ // by the render thread without taking the mutex.
116
+ std::chrono::steady_clock::time_point lastUpdateTime_{};
117
+ bool hasLastUpdate_{false};
118
+ std::atomic<double> ewmaIntervalSec_{0.0};
119
+ };
120
+
121
+ } // namespace reactnativecesium
@@ -5,6 +5,13 @@
5
5
 
6
6
  #include "CesiumEngine.hpp"
7
7
 
8
+ #ifdef __ANDROID__
9
+ #include <android/log.h>
10
+ #define ENGINE_LOGI(...) __android_log_print(ANDROID_LOG_INFO, "CesiumEngine", __VA_ARGS__)
11
+ #else
12
+ #define ENGINE_LOGI(...)
13
+ #endif
14
+
8
15
  #include <Cesium3DTilesContent/registerAllTileContentTypes.h>
9
16
  #include <Cesium3DTilesSelection/BoundingVolume.h>
10
17
  #include <Cesium3DTilesSelection/Tile.h>
@@ -17,8 +24,8 @@
17
24
  #include <CesiumAsync/SqliteCache.h>
18
25
  #include <CesiumCurl/CurlAssetAccessor.h>
19
26
  #include <CesiumRasterOverlays/IonRasterOverlay.h>
27
+ #include <CesiumRasterOverlays/RasterOverlay.h>
20
28
  #include <CesiumUtility/CreditSystem.h>
21
- #include <CesiumUtility/IntrusivePointer.h>
22
29
 
23
30
  #ifdef CESIUM_ENGINE_UNDEF_NDEBUG
24
31
  # undef NDEBUG
@@ -31,24 +38,30 @@
31
38
 
32
39
  #include <algorithm>
33
40
  #include <cmath>
41
+ #include <thread>
34
42
 
35
43
  namespace reactnativecesium {
36
44
 
45
+ int32_t CesiumEngine::resolveTaskThreadCount(int32_t requested) {
46
+ if (requested > 0) return requested;
47
+ unsigned int hw = std::thread::hardware_concurrency();
48
+ if (hw == 0) return 4;
49
+ // Reserve one core for the UI / display thread; clamp into a sensible band.
50
+ int32_t target = static_cast<int32_t>(hw) - 1;
51
+ if (target < 2) target = 2;
52
+ if (target > 8) target = 8;
53
+ return target;
54
+ }
55
+
37
56
  CesiumEngine::CesiumEngine()
38
- : taskProcessor_(std::make_shared<TaskProcessor>(4)),
57
+ : taskProcessor_(std::make_shared<TaskProcessor>(
58
+ static_cast<uint32_t>(resolveTaskThreadCount(0)))),
39
59
  asyncSystem_(taskProcessor_) {
40
60
  Cesium3DTilesContent::registerAllTileContentTypes();
41
61
  }
42
62
 
43
63
  CesiumEngine::~CesiumEngine() { shutdown(); }
44
64
 
45
- bool CesiumEngine::tilesetOptionsMatch(const EngineConfig& a,
46
- const EngineConfig& b) const {
47
- return a.maximumScreenSpaceError == b.maximumScreenSpaceError &&
48
- a.maximumSimultaneousTileLoads == b.maximumSimultaneousTileLoads &&
49
- a.loadingDescendantLimit == b.loadingDescendantLimit;
50
- }
51
-
52
65
  void CesiumEngine::buildEllipsoidMesh() {
53
66
  // Tessellate a WGS84 ellipsoid slightly inset so terrain tiles (at true
54
67
  // surface height) reliably overdraw it via the reversed-Z depth test.
@@ -92,11 +105,6 @@ void CesiumEngine::buildEllipsoidMesh() {
92
105
  }
93
106
  }
94
107
 
95
- // Append the fallback ellipsoid as a draw.
96
- // RTC convention for the fallback: the tile's "RTC centre" is the camera
97
- // position itself, so local positions == old camera-relative positions. The
98
- // per-draw MVP is therefore the same rotation-only VP used before RTC was
99
- // introduced, keeping the fallback on the same code path.
100
108
  void CesiumEngine::appendEllipsoidDraws(FrameResult& result) const {
101
109
  if (ellipsoidPositions_.empty() || ellipsoidIndices_.empty()) return;
102
110
 
@@ -115,13 +123,10 @@ void CesiumEngine::appendEllipsoidDraws(FrameResult& result) const {
115
123
  float* uvOut = result.uvs.data() + baseVertex * 2;
116
124
 
117
125
  for (const auto& posEcef : ellipsoidPositions_) {
118
- // Local position = ECEF - camera (same as old eye-relative, since camera is the RTC centre).
119
126
  const glm::dvec3 local = posEcef - cameraPos;
120
127
  *posOut++ = static_cast<float>(local.x);
121
128
  *posOut++ = static_cast<float>(local.y);
122
129
  *posOut++ = static_cast<float>(local.z);
123
- // Fallback geoid should represent land by default when terrain tiles are not
124
- // loaded yet, so keep altitude at sea level for hypsometric land colouring.
125
130
  *altOut++ = 0.0f;
126
131
  *uvOut++ = 0.5f;
127
132
  *uvOut++ = 0.5f;
@@ -140,8 +145,6 @@ void CesiumEngine::appendEllipsoidDraws(FrameResult& result) const {
140
145
  draw.hasUVs = false;
141
146
  draw.isEllipsoidFallback = true;
142
147
  draw.overlayTexture = nullptr;
143
- // Ellipsoid RTC centre = camera → local pos = eye-relative → use the
144
- // rotation-only VP (identical to the pre-RTC vertex transform).
145
148
  draw.mvpMatrix = result.vpMatrix;
146
149
  draw.rtcCenterEcef = result.cameraEcef;
147
150
  result.draws.push_back(draw);
@@ -149,18 +152,39 @@ void CesiumEngine::appendEllipsoidDraws(FrameResult& result) const {
149
152
 
150
153
  void CesiumEngine::initialize(const EngineConfig& config) {
151
154
  config_ = config;
155
+ // Ensure ionImageryAssetId default is honoured if the caller passed 0/<0.
156
+ if (config_.ionImageryAssetId <= 0) {
157
+ config_.ionImageryAssetId = 1;
158
+ }
159
+
160
+ // Re-create the worker pool if the requested thread count differs from
161
+ // the autodetected default we used in the constructor.
162
+ const int32_t desiredThreads = resolveTaskThreadCount(config_.taskProcessorThreads);
163
+ if (taskProcessor_) {
164
+ // We can only resize the pool by replacing it, which would break
165
+ // asyncSystem_. Skip when it's the same to preserve in-flight tasks.
166
+ // Note: TaskProcessor stores its size privately, so we conservatively
167
+ // recreate only when an explicit override is provided.
168
+ if (config_.taskProcessorThreads > 0) {
169
+ taskProcessor_->waitUntilIdle();
170
+ taskProcessor_ = std::make_shared<TaskProcessor>(
171
+ static_cast<uint32_t>(desiredThreads));
172
+ asyncSystem_ = CesiumAsync::AsyncSystem(taskProcessor_);
173
+ }
174
+ }
152
175
 
153
176
  auto logger = spdlog::default_logger();
154
177
 
155
178
  CesiumCurl::CurlAssetAccessorOptions curlOpts;
156
- if (!config.tlsCaBundlePath.empty()) {
157
- curlOpts.certificateFile = config.tlsCaBundlePath;
179
+ if (!config_.tlsCaBundlePath.empty()) {
180
+ curlOpts.certificateFile = config_.tlsCaBundlePath;
158
181
  }
159
182
  auto curlAccessor = std::make_shared<CesiumCurl::CurlAssetAccessor>(curlOpts);
160
183
 
161
- if (!config.cacheDatabasePath.empty()) {
184
+ if (!config_.cacheDatabasePath.empty()) {
162
185
  auto cache = std::make_shared<CesiumAsync::SqliteCache>(
163
- logger, config.cacheDatabasePath, 4096);
186
+ logger, config_.cacheDatabasePath,
187
+ static_cast<uint64_t>(std::max<int32_t>(64, config_.sqliteCacheMaxRows)));
164
188
  assetAccessor_ = std::make_shared<CesiumAsync::CachingAssetAccessor>(
165
189
  logger, curlAccessor, cache);
166
190
  } else {
@@ -174,7 +198,7 @@ void CesiumEngine::initialize(const EngineConfig& config) {
174
198
  }
175
199
 
176
200
  buildEllipsoidMesh();
177
- createTileset(config.ionAccessToken, config.ionAssetId);
201
+ createTileset();
178
202
  }
179
203
 
180
204
  void CesiumEngine::shutdown() {
@@ -183,30 +207,51 @@ void CesiumEngine::shutdown() {
183
207
  }
184
208
 
185
209
  void CesiumEngine::updateConfig(const EngineConfig& config) {
210
+ // Only token / asset id changes force a full tileset rebuild. Everything
211
+ // else is mutated in place via tileset_->getOptions() to avoid the very
212
+ // expensive teardown + re-load from the root.
186
213
  const bool needRebuild =
187
214
  config.ionAccessToken != config_.ionAccessToken ||
188
- config.ionAssetId != config_.ionAssetId ||
189
- !tilesetOptionsMatch(config, config_);
215
+ config.ionAssetId != config_.ionAssetId;
216
+
217
+ // Imagery overlay is its own change set.
218
+ const bool overlayChanged =
219
+ config.ionImageryAssetId != config_.ionImageryAssetId;
190
220
 
191
221
  config_ = config;
222
+ if (config_.ionImageryAssetId <= 0) {
223
+ config_.ionImageryAssetId = 1;
224
+ }
192
225
 
193
- if (!needRebuild) return;
226
+ if (needRebuild) {
227
+ destroyTileset();
228
+ if (!config_.ionAccessToken.empty()) {
229
+ createTileset();
230
+ }
231
+ return;
232
+ }
194
233
 
195
- destroyTileset();
196
- if (!config_.ionAccessToken.empty()) {
197
- createTileset(config_.ionAccessToken, config_.ionAssetId, currentImageryAssetId_);
234
+ if (tileset_) {
235
+ applyRuntimeConfig(config_);
236
+ }
237
+
238
+ if (overlayChanged) {
239
+ applyImageryOverlay(config_.ionImageryAssetId);
198
240
  }
199
241
  }
200
242
 
201
- void CesiumEngine::createTileset(const std::string& token,
202
- int64_t assetId,
203
- int64_t imageryAssetId) {
204
- if (token.empty()) return;
243
+ void CesiumEngine::createTileset() {
244
+ if (config_.ionAccessToken.empty()) {
245
+ spdlog::warn("createTileset: ion access token is empty, skipping tileset creation");
246
+ return;
247
+ }
205
248
 
206
249
  if (!creditSystem_) {
207
250
  creditSystem_ = std::make_shared<CesiumUtility::CreditSystem>();
208
251
  }
209
252
 
253
+ spdlog::info("createTileset: creating tileset with assetId={}", config_.ionAssetId);
254
+
210
255
  auto logger = spdlog::default_logger();
211
256
 
212
257
  Cesium3DTilesSelection::TilesetExternals externals{
@@ -214,38 +259,141 @@ void CesiumEngine::createTileset(const std::string& token,
214
259
  logger, nullptr};
215
260
 
216
261
  Cesium3DTilesSelection::TilesetOptions opts;
217
- opts.maximumCachedBytes = 256 * 1024 * 1024;
262
+ opts.maximumCachedBytes = static_cast<int64_t>(
263
+ std::max<int64_t>(16LL * 1024LL * 1024LL, config_.maximumCachedBytes));
218
264
  opts.maximumSimultaneousTileLoads =
219
265
  static_cast<uint32_t>(std::max(0, config_.maximumSimultaneousTileLoads));
220
266
  opts.loadingDescendantLimit =
221
267
  static_cast<uint32_t>(std::max(1, config_.loadingDescendantLimit));
222
268
  opts.maximumScreenSpaceError =
223
269
  std::max(1.0, config_.maximumScreenSpaceError);
224
- opts.preloadAncestors = true;
225
- opts.preloadSiblings = true;
226
- opts.forbidHoles = true;
227
- opts.contentOptions.enableWaterMask = true;
270
+ opts.preloadAncestors = config_.preloadAncestors;
271
+ opts.preloadSiblings = config_.preloadSiblings;
272
+ opts.forbidHoles = config_.forbidHoles;
273
+ opts.contentOptions.enableWaterMask = config_.enableWaterMask;
274
+ opts.enableFogCulling = config_.enableFogCulling;
275
+ opts.enforceCulledScreenSpaceError = config_.enforceCulledScreenSpaceError;
276
+ opts.culledScreenSpaceError = std::max(1.0, config_.culledScreenSpaceError);
277
+ opts.enableLodTransitionPeriod = config_.enableLodTransitionPeriod;
278
+ opts.lodTransitionLength = std::max(0.0, config_.lodTransitionLength);
228
279
 
229
280
  tileset_ = std::make_unique<Cesium3DTilesSelection::Tileset>(
230
- externals, assetId, token, opts);
281
+ externals, config_.ionAssetId, config_.ionAccessToken, opts);
282
+
283
+ spdlog::info("createTileset: tileset created successfully");
284
+
285
+ currentOverlay_ = nullptr;
286
+ currentImageryAssetId_ = 1;
287
+ if (config_.ionImageryAssetId > 0 && config_.ionImageryAssetId != 1) {
288
+ applyImageryOverlay(config_.ionImageryAssetId);
289
+ }
290
+ }
231
291
 
232
- if (imageryAssetId != 1) {
292
+ bool CesiumEngine::applyRuntimeConfig(const EngineConfig& cfg) {
293
+ if (!tileset_) return false;
294
+ auto& opts = tileset_->getOptions();
295
+ bool changed = false;
296
+
297
+ const double newSse =
298
+ std::max(1.0, cfg.maximumScreenSpaceError);
299
+ if (opts.maximumScreenSpaceError != newSse) {
300
+ opts.maximumScreenSpaceError = newSse;
301
+ changed = true;
302
+ }
303
+ const uint32_t newSim =
304
+ static_cast<uint32_t>(std::max(0, cfg.maximumSimultaneousTileLoads));
305
+ if (opts.maximumSimultaneousTileLoads != newSim) {
306
+ opts.maximumSimultaneousTileLoads = newSim;
307
+ changed = true;
308
+ }
309
+ const uint32_t newDesc =
310
+ static_cast<uint32_t>(std::max(1, cfg.loadingDescendantLimit));
311
+ if (opts.loadingDescendantLimit != newDesc) {
312
+ opts.loadingDescendantLimit = newDesc;
313
+ changed = true;
314
+ }
315
+ const int64_t newCacheBytes = std::max<int64_t>(
316
+ 16LL * 1024LL * 1024LL, cfg.maximumCachedBytes);
317
+ if (opts.maximumCachedBytes != newCacheBytes) {
318
+ opts.maximumCachedBytes = newCacheBytes;
319
+ changed = true;
320
+ }
321
+ if (opts.preloadAncestors != cfg.preloadAncestors) {
322
+ opts.preloadAncestors = cfg.preloadAncestors;
323
+ changed = true;
324
+ }
325
+ if (opts.preloadSiblings != cfg.preloadSiblings) {
326
+ opts.preloadSiblings = cfg.preloadSiblings;
327
+ changed = true;
328
+ }
329
+ if (opts.forbidHoles != cfg.forbidHoles) {
330
+ opts.forbidHoles = cfg.forbidHoles;
331
+ changed = true;
332
+ }
333
+ // Note: contentOptions.enableWaterMask only takes effect on tiles loaded
334
+ // after the change. Existing in-memory tiles keep their water mask state.
335
+ if (opts.contentOptions.enableWaterMask != cfg.enableWaterMask) {
336
+ opts.contentOptions.enableWaterMask = cfg.enableWaterMask;
337
+ changed = true;
338
+ }
339
+ if (opts.enableFogCulling != cfg.enableFogCulling) {
340
+ opts.enableFogCulling = cfg.enableFogCulling;
341
+ changed = true;
342
+ }
343
+ if (opts.enforceCulledScreenSpaceError != cfg.enforceCulledScreenSpaceError) {
344
+ opts.enforceCulledScreenSpaceError = cfg.enforceCulledScreenSpaceError;
345
+ changed = true;
346
+ }
347
+ const double newCulled =
348
+ std::max(1.0, cfg.culledScreenSpaceError);
349
+ if (opts.culledScreenSpaceError != newCulled) {
350
+ opts.culledScreenSpaceError = newCulled;
351
+ changed = true;
352
+ }
353
+ if (opts.enableLodTransitionPeriod != cfg.enableLodTransitionPeriod) {
354
+ opts.enableLodTransitionPeriod = cfg.enableLodTransitionPeriod;
355
+ changed = true;
356
+ }
357
+ const double newLodLen = std::max(0.0, cfg.lodTransitionLength);
358
+ if (opts.lodTransitionLength != newLodLen) {
359
+ opts.lodTransitionLength = newLodLen;
360
+ changed = true;
361
+ }
362
+ return changed;
363
+ }
364
+
365
+ void CesiumEngine::applyImageryOverlay(int64_t targetAssetId) {
366
+ if (!tileset_) return;
367
+ if (targetAssetId == currentImageryAssetId_) return;
368
+
369
+ if (currentOverlay_) {
370
+ tileset_->getOverlays().remove(currentOverlay_);
371
+ currentOverlay_ = nullptr;
372
+ }
373
+ currentImageryAssetId_ = (targetAssetId <= 0) ? 1 : targetAssetId;
374
+
375
+ if (currentImageryAssetId_ != 1 && !config_.ionAccessToken.empty()) {
233
376
  CesiumUtility::IntrusivePointer<CesiumRasterOverlays::RasterOverlay> ov =
234
- new CesiumRasterOverlays::IonRasterOverlay("imagery", imageryAssetId, token);
377
+ new CesiumRasterOverlays::IonRasterOverlay(
378
+ "imagery", currentImageryAssetId_, config_.ionAccessToken);
235
379
  tileset_->getOverlays().add(ov);
380
+ currentOverlay_ = ov;
236
381
  }
237
382
  }
238
383
 
239
384
  void CesiumEngine::destroyTileset() {
385
+ currentOverlay_ = nullptr;
386
+ currentImageryAssetId_ = 1;
240
387
  tileset_.reset();
241
- taskProcessor_->waitUntilIdle();
388
+ if (taskProcessor_) {
389
+ taskProcessor_->waitUntilIdle();
390
+ }
242
391
  }
243
392
 
244
393
  void CesiumEngine::setImageryAssetId(int64_t assetId) {
245
394
  if (config_.ionAccessToken.empty()) return;
246
- currentImageryAssetId_ = assetId;
247
- destroyTileset();
248
- createTileset(config_.ionAccessToken, config_.ionAssetId, assetId);
395
+ config_.ionImageryAssetId = (assetId <= 0) ? 1 : assetId;
396
+ applyImageryOverlay(config_.ionImageryAssetId);
249
397
  }
250
398
 
251
399
  void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
@@ -278,20 +426,24 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
278
426
  return;
279
427
  }
280
428
 
281
- result.localPositions.reserve(512 * 1024);
282
- result.altitudes.reserve(512 * 1024 / 3);
283
- result.uvs.reserve(512 * 1024 * 2);
284
- result.indices.reserve(3 * 1024 * 1024);
429
+ result.localPositions.reserve(tunables::kFrameResultPositionFloatReserve);
430
+ result.altitudes.reserve(tunables::kFrameResultPositionFloatReserve / 3);
431
+ result.uvs.reserve(tunables::kFrameResultPositionFloatReserve * 2);
432
+ result.indices.reserve(tunables::kFrameResultIndexUint32Reserve);
285
433
 
286
434
  asyncSystem_.dispatchMainThreadTasks();
287
435
 
288
- // Ellipsoid fallback is appended first so real terrain tiles (drawn later)
289
- // overdraw it via the reversed-Z depth test.
290
- appendEllipsoidDraws(result);
291
-
292
436
  const auto viewState = camera_.computeViewState(w, h);
293
437
  const auto& updateResult = tileset_->updateView({viewState});
294
438
 
439
+ // Only emit the fallback ellipsoid when no real terrain is being rendered
440
+ // this frame. Otherwise it would be over-drawn anyway (~8k vertices and
441
+ // ~24k indices of throwaway transform + memcpy per frame).
442
+ const bool noVisibleTerrain = updateResult.tilesToRenderThisFrame.empty();
443
+ if (noVisibleTerrain) {
444
+ appendEllipsoidDraws(result);
445
+ }
446
+
295
447
  if (creditSystem_) {
296
448
  const CesiumUtility::CreditsSnapshot& snap =
297
449
  creditSystem_->getSnapshot(CesiumUtility::CreditFilteringMode::UniqueHtml);
@@ -339,8 +491,6 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
339
491
  !prim.uvs.empty() && prim.uvs.size() == vertexCount;
340
492
 
341
493
  // ── RTC: copy tile-local positions & altitudes ────────────────────────
342
- // Positions are already relative to prim.rtcCenter (small float values).
343
- // Altitudes are computed in double in GltfToMesh — sub-millimetre precision.
344
494
  result.localPositions.resize(result.localPositions.size() + vertexCount * 3);
345
495
  result.altitudes.resize(result.altitudes.size() + vertexCount);
346
496
  result.uvs.resize(result.uvs.size() + vertexCount * 2);
@@ -369,10 +519,6 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
369
519
  }
370
520
 
371
521
  // ── RTC: per-tile MVP matrix (double → float) ─────────────────────────
372
- // Compute the camera-relative offset of the tile's RTC centre in double
373
- // and bake it into the MVP matrix before casting to float32. This means
374
- // the large camera↔tile translation is resolved with double precision;
375
- // the vertex shader only ever adds small tile-local offsets on top of it.
376
522
  const glm::dvec3 rtcCamRel =
377
523
  prim.rtcCenter - glm::dvec3(cameraPos);
378
524
  const glm::dmat4 translateD =
@@ -1,6 +1,7 @@
1
1
  #pragma once
2
2
 
3
3
  #include "../renderer/RenderTypes.hpp"
4
+ #include "EngineTunables.hpp"
4
5
  #include "GlobeCamera.hpp"
5
6
  #include "ResourcePreparer.hpp"
6
7
  #include "TaskProcessor.hpp"
@@ -8,6 +9,7 @@
8
9
 
9
10
  #include <CesiumAsync/AsyncSystem.h>
10
11
  #include <CesiumAsync/IAssetAccessor.h>
12
+ #include <CesiumUtility/IntrusivePointer.h>
11
13
 
12
14
  #include <memory>
13
15
  #include <string>
@@ -20,16 +22,62 @@ namespace CesiumUtility {
20
22
  class CreditSystem;
21
23
  }
22
24
 
25
+ namespace CesiumRasterOverlays {
26
+ class RasterOverlay;
27
+ }
28
+
23
29
  namespace reactnativecesium {
24
30
 
31
+ // Mirror of Cesium Native TilesetOptions / SqliteCache + a few of our own
32
+ // knobs. Default values match the values previously hardcoded in
33
+ // CesiumEngine.cpp so existing call sites that pass an EngineConfig with only
34
+ // the few "core" fields populated continue to behave as before.
25
35
  struct EngineConfig {
36
+ // ── Cesium Ion / network ──────────────────────────────────────────────
26
37
  std::string ionAccessToken;
27
- int64_t ionAssetId = 1;
38
+ int64_t ionAssetId = 1;
39
+ int64_t ionImageryAssetId = 1; // 1 == "no overlay (use built-in colours)"
28
40
  std::string cacheDatabasePath;
29
41
  std::string tlsCaBundlePath;
30
- double maximumScreenSpaceError = 32.0;
31
- int32_t maximumSimultaneousTileLoads = 12;
32
- int32_t loadingDescendantLimit = 20;
42
+
43
+ // ── Tileset selection (TilesetOptions) ───────────────────────────────
44
+ double maximumScreenSpaceError = tunables::kDefaultMaximumScreenSpaceError;
45
+ int32_t maximumSimultaneousTileLoads = tunables::kDefaultMaximumSimultaneousTileLoads;
46
+ int32_t loadingDescendantLimit = tunables::kDefaultLoadingDescendantLimit;
47
+
48
+ // RAM budget for decoded geometry/textures held by the live tileset.
49
+ int64_t maximumCachedBytes = tunables::kDefaultMaxCachedBytes;
50
+
51
+ // Pre-load extra tiles around what is strictly needed for this view —
52
+ // smoothes panning / hides pop-in but increases peak load pressure.
53
+ bool preloadAncestors = true;
54
+ bool preloadSiblings = true;
55
+
56
+ // Refuse to render holes in the terrain (Cesium will keep ancestor tiles
57
+ // visible until all children load). Relax during fast pans to lower
58
+ // upload pressure.
59
+ bool forbidHoles = true;
60
+
61
+ // Water-mask textures (carries coastline shading data). Cheap to disable on
62
+ // memory-constrained devices.
63
+ bool enableWaterMask = true;
64
+
65
+ // ── Optional / off-by-default selection knobs ────────────────────────
66
+ bool enableFogCulling = false;
67
+ bool enforceCulledScreenSpaceError = true;
68
+ double culledScreenSpaceError = 64.0;
69
+ bool enableLodTransitionPeriod = false;
70
+ double lodTransitionLength = 1.0;
71
+ // Seconds an unused tile remains in memory before being evicted.
72
+ double tileCacheUnloadTimeInSeconds = 5.0;
73
+
74
+ // ── Disk cache (SqliteCache) ─────────────────────────────────────────
75
+ int32_t sqliteCacheMaxRows = tunables::kDefaultSqliteCacheMaxRows;
76
+
77
+ // ── Worker pool ──────────────────────────────────────────────────────
78
+ // 0 means "auto-detect" via std::thread::hardware_concurrency() clamped to
79
+ // [2, 8]. Override only for benchmarking / regression testing.
80
+ int32_t taskProcessorThreads = 0;
33
81
  };
34
82
 
35
83
  class CesiumEngine {
@@ -42,6 +90,12 @@ public:
42
90
 
43
91
  void initialize(const EngineConfig& config);
44
92
  void shutdown();
93
+
94
+ // Apply config changes. Splits cleanly into:
95
+ // - "tileset must be rebuilt" (token / assetId changed) → destroy + recreate,
96
+ // - "runtime-mutable" (SSE / load limits / cache bytes / ...) →
97
+ // mutate tileset_->getOptions() in place,
98
+ // - "imagery overlay only" → IRasterOverlay add/remove.
45
99
  void updateConfig(const EngineConfig& config);
46
100
 
47
101
  void updateFrame(double viewportWidth, double viewportHeight, FrameResult& result);
@@ -53,17 +107,26 @@ public:
53
107
 
54
108
  ResourcePreparer* getResourcePreparer() const { return resourcePreparer_.get(); }
55
109
 
110
+ // Expose a few useful read-only bits to platform bridges (avoids leaking
111
+ // EngineConfig everywhere).
112
+ bool tilesetReady() const { return tileset_ != nullptr; }
113
+ int64_t currentImageryAssetId() const { return currentImageryAssetId_; }
114
+
56
115
  private:
57
- void createTileset(const std::string& token,
58
- int64_t assetId,
59
- int64_t imageryAssetId = 1);
116
+ void createTileset();
60
117
  void destroyTileset();
61
118
 
62
- bool tilesetOptionsMatch(const EngineConfig& a, const EngineConfig& b) const;
119
+ // Apply runtime-mutable bits of `cfg` to the live tileset. Returns true if a
120
+ // value actually changed (callers can use this to trigger force-render).
121
+ bool applyRuntimeConfig(const EngineConfig& cfg);
122
+
123
+ // Apply imagery overlay change (target assetId of 1 means "no overlay").
124
+ void applyImageryOverlay(int64_t targetAssetId);
125
+
126
+ static int32_t resolveTaskThreadCount(int32_t requested);
63
127
 
64
128
  // Builds a tessellated WGS84 ellipsoid mesh (inset ~20 m) used as a
65
- // permanent background so flat terrain is always visible even when tiles
66
- // are absent or not yet loaded.
129
+ // fallback background while terrain tiles are not yet ready.
67
130
  void buildEllipsoidMesh();
68
131
  void appendEllipsoidDraws(FrameResult& result) const;
69
132
 
@@ -77,6 +140,10 @@ private:
77
140
 
78
141
  std::unique_ptr<Cesium3DTilesSelection::Tileset> tileset_;
79
142
 
143
+ // Currently-active overlay — kept so we can remove it cheaply when the
144
+ // imagery asset id changes.
145
+ CesiumUtility::IntrusivePointer<CesiumRasterOverlays::RasterOverlay>
146
+ currentOverlay_;
80
147
  int64_t currentImageryAssetId_ = 1;
81
148
 
82
149
  GlobeCamera camera_;