react-native-cesium 1.2.6 → 1.2.8
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/android/CMakeLists.txt +24 -9
- package/android/src/main/cpp/vulkan/VulkanBackend.cpp +166 -67
- package/android/src/main/cpp/vulkan/VulkanBackend.h +60 -19
- package/android/src/main/cpp/vulkan/shaders/terrain.frag +36 -8
- package/android/src/main/java/com/margelo/nitro/reactnativecesium/HybridCesiumView.kt +14 -1
- package/cpp/engine/CameraIntegrator.cpp +32 -10
- package/cpp/engine/CameraIntegrator.hpp +1 -1
- package/cpp/engine/CesiumEngine.cpp +487 -34
- package/cpp/engine/CesiumEngine.hpp +73 -4
- package/cpp/engine/EngineTunables.hpp +7 -0
- package/cpp/renderer/RenderTypes.hpp +13 -0
- package/ios/CesiumBridge.h +9 -3
- package/ios/CesiumBridge.mm +60 -5
- package/ios/HybridCesiumView.swift +25 -8
- package/ios/metal/MetalBackend.h +22 -2
- package/ios/metal/MetalBackend.mm +248 -93
- package/package.json +1 -1
|
@@ -5,12 +5,6 @@
|
|
|
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
9
|
#include <Cesium3DTilesContent/registerAllTileContentTypes.h>
|
|
16
10
|
#include <Cesium3DTilesSelection/BoundingVolume.h>
|
|
@@ -38,8 +32,10 @@
|
|
|
38
32
|
|
|
39
33
|
#include <algorithm>
|
|
40
34
|
#include <cmath>
|
|
35
|
+
#include <cstring>
|
|
41
36
|
#include <thread>
|
|
42
37
|
|
|
38
|
+
|
|
43
39
|
namespace reactnativecesium {
|
|
44
40
|
|
|
45
41
|
int32_t CesiumEngine::resolveTaskThreadCount(int32_t requested) {
|
|
@@ -47,9 +43,15 @@ int32_t CesiumEngine::resolveTaskThreadCount(int32_t requested) {
|
|
|
47
43
|
unsigned int hw = std::thread::hardware_concurrency();
|
|
48
44
|
if (hw == 0) return 4;
|
|
49
45
|
// Reserve one core for the UI / display thread; clamp into a sensible band.
|
|
46
|
+
// The upper bound was 8 historically but modern phones / tablets routinely
|
|
47
|
+
// ship 8-10 cores (iPhone 15 Pro, iPad M2/M4, Pixel 8 Pro, Galaxy S24
|
|
48
|
+
// Ultra) and tile decoding (Draco, KTX2, glTF parse) is embarrassingly
|
|
49
|
+
// parallel — capping at 8 left half the CPU idle during cold-start tile
|
|
50
|
+
// burst. 16 covers everything up to a Mac M4 Pro and is harmless on
|
|
51
|
+
// smaller chips because we simply create empty workers.
|
|
50
52
|
int32_t target = static_cast<int32_t>(hw) - 1;
|
|
51
|
-
if (target < 2)
|
|
52
|
-
if (target >
|
|
53
|
+
if (target < 2) target = 2;
|
|
54
|
+
if (target > 16) target = 16;
|
|
53
55
|
return target;
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -276,6 +278,10 @@ void CesiumEngine::createTileset() {
|
|
|
276
278
|
opts.culledScreenSpaceError = std::max(1.0, config_.culledScreenSpaceError);
|
|
277
279
|
opts.enableLodTransitionPeriod = config_.enableLodTransitionPeriod;
|
|
278
280
|
opts.lodTransitionLength = std::max(0.0, config_.lodTransitionLength);
|
|
281
|
+
// Rate-limit per-frame main-thread tile finalisation so the render thread
|
|
282
|
+
// stays responsive during heavy startup bursts (see EngineConfig docs).
|
|
283
|
+
opts.mainThreadLoadingTimeLimit =
|
|
284
|
+
std::max(0.0, config_.mainThreadLoadingTimeLimitMs);
|
|
279
285
|
|
|
280
286
|
tileset_ = std::make_unique<Cesium3DTilesSelection::Tileset>(
|
|
281
287
|
externals, config_.ionAssetId, config_.ionAccessToken, opts);
|
|
@@ -359,6 +365,11 @@ bool CesiumEngine::applyRuntimeConfig(const EngineConfig& cfg) {
|
|
|
359
365
|
opts.lodTransitionLength = newLodLen;
|
|
360
366
|
changed = true;
|
|
361
367
|
}
|
|
368
|
+
const double newMainLimit = std::max(0.0, cfg.mainThreadLoadingTimeLimitMs);
|
|
369
|
+
if (opts.mainThreadLoadingTimeLimit != newMainLimit) {
|
|
370
|
+
opts.mainThreadLoadingTimeLimit = newMainLimit;
|
|
371
|
+
changed = true;
|
|
372
|
+
}
|
|
362
373
|
return changed;
|
|
363
374
|
}
|
|
364
375
|
|
|
@@ -379,6 +390,18 @@ void CesiumEngine::applyImageryOverlay(int64_t targetAssetId) {
|
|
|
379
390
|
tileset_->getOverlays().add(ov);
|
|
380
391
|
currentOverlay_ = ov;
|
|
381
392
|
}
|
|
393
|
+
|
|
394
|
+
// an overlay swap mutates res->overlayTexture pointers on already-loaded
|
|
395
|
+
// tiles. The fast path re-reads the texture from `res` each frame so output
|
|
396
|
+
// pixels stay correct, but burn one stale frame to be safe: drop the cache
|
|
397
|
+
// and force the engine to take the slow path again until N stable frames
|
|
398
|
+
// confirm the new state.
|
|
399
|
+
lastDrawSignature_ = 0;
|
|
400
|
+
stableSigFrames_ = 0;
|
|
401
|
+
cachedDraws_.clear();
|
|
402
|
+
// floor cache is keyed on signature — drop it too so the next frame
|
|
403
|
+
// re-scans before establishing a new entry.
|
|
404
|
+
floorCacheValid_ = false;
|
|
382
405
|
}
|
|
383
406
|
|
|
384
407
|
void CesiumEngine::destroyTileset() {
|
|
@@ -388,6 +411,14 @@ void CesiumEngine::destroyTileset() {
|
|
|
388
411
|
if (taskProcessor_) {
|
|
389
412
|
taskProcessor_->waitUntilIdle();
|
|
390
413
|
}
|
|
414
|
+
// every TileGPUResources* baked into cachedDraws_ has just been freed.
|
|
415
|
+
// Clearing the cache guarantees no dangling pointer survives into the next
|
|
416
|
+
// updateFrame and forces a clean slow-path rebuild.
|
|
417
|
+
lastDrawSignature_ = 0;
|
|
418
|
+
stableSigFrames_ = 0;
|
|
419
|
+
cachedDraws_.clear();
|
|
420
|
+
// P5: the cached value was keyed on TileGPUResources we just freed.
|
|
421
|
+
floorCacheValid_ = false;
|
|
391
422
|
}
|
|
392
423
|
|
|
393
424
|
void CesiumEngine::setImageryAssetId(int64_t assetId) {
|
|
@@ -396,6 +427,19 @@ void CesiumEngine::setImageryAssetId(int64_t assetId) {
|
|
|
396
427
|
applyImageryOverlay(config_.ionImageryAssetId);
|
|
397
428
|
}
|
|
398
429
|
|
|
430
|
+
namespace {
|
|
431
|
+
// FNV-1a 64-bit hash mix. Used to compute the geometry signature from the
|
|
432
|
+
// ordered list of (TileGPUResources*, primitive*, primitive.indices.size())
|
|
433
|
+
// triples for tilesToRenderThisFrame. Two frames whose visible primitives
|
|
434
|
+
// hash to the same value are guaranteed (modulo astronomically unlikely
|
|
435
|
+
// 64-bit collision) to produce identical merged vertex / index / UV buffers.
|
|
436
|
+
inline uint64_t fnv1a64Mix(uint64_t h, uint64_t v) {
|
|
437
|
+
h ^= v;
|
|
438
|
+
h *= 0x100000001b3ULL;
|
|
439
|
+
return h;
|
|
440
|
+
}
|
|
441
|
+
} // namespace
|
|
442
|
+
|
|
399
443
|
void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
|
|
400
444
|
result.localPositions.clear();
|
|
401
445
|
result.altitudes.clear();
|
|
@@ -403,6 +447,7 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
|
|
|
403
447
|
result.indices.clear();
|
|
404
448
|
result.draws.clear();
|
|
405
449
|
result.creditHtmlLines.clear();
|
|
450
|
+
result.geometrySignature = 0;
|
|
406
451
|
|
|
407
452
|
result.ionTokenConfigured = !config_.ionAccessToken.empty();
|
|
408
453
|
result.tilesetActive = (tileset_ != nullptr);
|
|
@@ -422,6 +467,9 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
|
|
|
422
467
|
|
|
423
468
|
if (!tileset_) {
|
|
424
469
|
appendEllipsoidDraws(result);
|
|
470
|
+
lastDrawSignature_ = 0;
|
|
471
|
+
stableSigFrames_ = 0;
|
|
472
|
+
cachedDraws_.clear();
|
|
425
473
|
lifecycle_.advanceFrame();
|
|
426
474
|
return;
|
|
427
475
|
}
|
|
@@ -433,9 +481,22 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
|
|
|
433
481
|
|
|
434
482
|
asyncSystem_.dispatchMainThreadTasks();
|
|
435
483
|
|
|
484
|
+
// Wall-clock deltaTime since previous frame. Cesium Native requires this to
|
|
485
|
+
// advance LOD transition fade percentages (without it, fade stays at 0 and
|
|
486
|
+
// newly-selected tiles never become visible). Clamped to a max of 100ms so a
|
|
487
|
+
// long pause / app foreground transition doesn't instantly complete fades.
|
|
488
|
+
const auto now = std::chrono::steady_clock::now();
|
|
489
|
+
float deltaTime = 0.0f;
|
|
490
|
+
if (hasLastFrameTime_) {
|
|
491
|
+
const auto diff = std::chrono::duration<float>(now - lastFrameTime_).count();
|
|
492
|
+
deltaTime = std::min(diff, 0.1f);
|
|
493
|
+
}
|
|
494
|
+
lastFrameTime_ = now;
|
|
495
|
+
hasLastFrameTime_ = true;
|
|
496
|
+
|
|
436
497
|
const auto viewState = camera_.computeViewState(w, h);
|
|
437
498
|
const auto& updateResult =
|
|
438
|
-
tileset_->updateViewGroup(tileset_->getDefaultViewGroup(), {viewState});
|
|
499
|
+
tileset_->updateViewGroup(tileset_->getDefaultViewGroup(), {viewState}, deltaTime);
|
|
439
500
|
tileset_->loadTiles();
|
|
440
501
|
|
|
441
502
|
// Only emit the fallback ellipsoid when no real terrain is being rendered
|
|
@@ -444,6 +505,12 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
|
|
|
444
505
|
const bool noVisibleTerrain = updateResult.tilesToRenderThisFrame.empty();
|
|
445
506
|
if (noVisibleTerrain) {
|
|
446
507
|
appendEllipsoidDraws(result);
|
|
508
|
+
// Ellipsoid fallback emits its own geometry; any cached terrain state is
|
|
509
|
+
// stale until real tiles arrive. Reset so fast path can't fire on the
|
|
510
|
+
// very next frame after a tile re-appears.
|
|
511
|
+
lastDrawSignature_ = 0;
|
|
512
|
+
stableSigFrames_ = 0;
|
|
513
|
+
cachedDraws_.clear();
|
|
447
514
|
}
|
|
448
515
|
|
|
449
516
|
if (creditSystem_) {
|
|
@@ -457,6 +524,81 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
|
|
|
457
524
|
}
|
|
458
525
|
}
|
|
459
526
|
|
|
527
|
+
// ── geometry signature ────────────────────────────────────────────
|
|
528
|
+
// Fingerprint the ordered visible primitive list. When the same fingerprint
|
|
529
|
+
// shows up for kMaxFramesInFlight consecutive frames *and* there are no
|
|
530
|
+
// LOD-fade draws, every backend slot's persistent buffer already holds the
|
|
531
|
+
// right bytes — the fast path can then skip the entire merge loop and the
|
|
532
|
+
// memcpys it produces.
|
|
533
|
+
uint64_t signature = 0xcbf29ce484222325ULL; // FNV-1a 64-bit offset basis
|
|
534
|
+
if (!noVisibleTerrain) {
|
|
535
|
+
for (const auto& tile : updateResult.tilesToRenderThisFrame) {
|
|
536
|
+
if (!tile) continue;
|
|
537
|
+
const auto* rc = tile->getContent().getRenderContent();
|
|
538
|
+
if (!rc) continue;
|
|
539
|
+
const auto* res = static_cast<const TileGPUResources*>(rc->getRenderResources());
|
|
540
|
+
if (!res) continue;
|
|
541
|
+
signature = fnv1a64Mix(signature, reinterpret_cast<uint64_t>(res));
|
|
542
|
+
signature = fnv1a64Mix(signature,
|
|
543
|
+
static_cast<uint64_t>(res->primitives.size()));
|
|
544
|
+
for (const auto& prim : res->primitives) {
|
|
545
|
+
signature = fnv1a64Mix(signature, reinterpret_cast<uint64_t>(&prim));
|
|
546
|
+
signature = fnv1a64Mix(signature,
|
|
547
|
+
static_cast<uint64_t>(prim.indices.size()));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// Reserve 0 as the "force upload" sentinel; we should never emit 0 as a
|
|
551
|
+
// valid signature.
|
|
552
|
+
if (signature == 0ULL) signature = 1ULL;
|
|
553
|
+
} else {
|
|
554
|
+
signature = 0ULL;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// ── scan tilesFadingOut for any in-flight fades ──────────────────────
|
|
558
|
+
// Fast path is illegal whenever a single fading-out draw would be emitted
|
|
559
|
+
// (lodFade < 1 means the per-tile dither cutoff changes every frame and the
|
|
560
|
+
// emitted set of draws may grow/shrink mid-transition).
|
|
561
|
+
bool anyFadeDraws = false;
|
|
562
|
+
for (const auto& tile : updateResult.tilesFadingOut) {
|
|
563
|
+
if (!tile) continue;
|
|
564
|
+
const auto* rc = tile->getContent().getRenderContent();
|
|
565
|
+
if (!rc) continue;
|
|
566
|
+
if (1.0f - rc->getLodTransitionFadePercentage() > 0.0f) {
|
|
567
|
+
anyFadeDraws = true;
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ── update stability counter ─────────────────────────────────────────
|
|
573
|
+
// IMPORTANT: reset to 0, NOT 1. Setting it to 1 was an off-by-one that
|
|
574
|
+
// let the fast path fire while one slot still held the previous signature:
|
|
575
|
+
// fi=A (reset to 1): slow path → geomSlotSig_[A]=sig
|
|
576
|
+
// fi=B (counter=2) : slow path → geomSlotSig_[B]=sig
|
|
577
|
+
// fi=C (counter=3≥3): fast path → geomSlotSig_[C] STILL stale → blank!
|
|
578
|
+
// With reset=0 the fast path is deferred until ALL kMaxFramesInFlight
|
|
579
|
+
// slots have been written with the current signature.
|
|
580
|
+
if (!noVisibleTerrain && signature != 0ULL) {
|
|
581
|
+
if (signature == lastDrawSignature_) {
|
|
582
|
+
++stableSigFrames_;
|
|
583
|
+
} else {
|
|
584
|
+
stableSigFrames_ = 0;
|
|
585
|
+
lastDrawSignature_ = signature;
|
|
586
|
+
cachedDraws_.clear();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// NOTE: must be STRICTLY GREATER (>) than kMaxFramesInFlight, not >=.
|
|
591
|
+
// With >= kMaxFramesInFlight the fast path fires on the Nth stable frame,
|
|
592
|
+
// which is exactly the frame where the Nth backend slot *would* have been
|
|
593
|
+
// written in the slow path. At that point the slot still holds the previous
|
|
594
|
+
// (or zero) signature and the backend returns a blank frame.
|
|
595
|
+
// With > kMaxFramesInFlight the Nth slow-path frame writes the last slot,
|
|
596
|
+
// and the fast path activates on frame N+1 when all slots are primed.
|
|
597
|
+
const bool canFastPath = !noVisibleTerrain && !anyFadeDraws &&
|
|
598
|
+
signature != 0ULL &&
|
|
599
|
+
stableSigFrames_ > tunables::kMaxFramesInFlight &&
|
|
600
|
+
!cachedDraws_.empty();
|
|
601
|
+
|
|
460
602
|
// ── Terrain floor tracking (for minAltitudeAboveTerrain clamp) ───────────
|
|
461
603
|
// Computed once before the tile loop and updated per-tile inside it.
|
|
462
604
|
// Only active when the feature is enabled to avoid any overhead otherwise.
|
|
@@ -467,6 +609,125 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
|
|
|
467
609
|
float bestTerrainFloor = terrainFloorEllipsoidM_; // carry previous value if no tile matches
|
|
468
610
|
double bestTerrainDist = std::numeric_limits<double>::max();
|
|
469
611
|
|
|
612
|
+
// ── floor-scan cache lookup ─────────────────────────────────────────
|
|
613
|
+
// Skip the per-vertex scan when the camera has moved less than ~10 cm in
|
|
614
|
+
// both lat and lon AND the visible primitive set is unchanged (same
|
|
615
|
+
// geometry signature). The cached value is only trusted for non-zero
|
|
616
|
+
// signatures — sig==0 means a fade or rebuild is in flight, in which case
|
|
617
|
+
// we always re-scan and refresh the cache afterwards.
|
|
618
|
+
bool floorScanFromCache = false;
|
|
619
|
+
if (trackTerrainFloor && floorCacheValid_ && signature != 0ULL &&
|
|
620
|
+
signature == floorCacheSignature_) {
|
|
621
|
+
const double dLon = std::abs(camParams.longitude - floorCacheCamLonDeg_);
|
|
622
|
+
const double dLat = std::abs(camParams.latitude - floorCacheCamLatDeg_);
|
|
623
|
+
if (dLon < 1e-6 && dLat < 1e-6) {
|
|
624
|
+
bestTerrainFloor = floorCacheValue_;
|
|
625
|
+
floorScanFromCache = true;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// When tracking is off, suppress the per-tile scan branches (legacy
|
|
629
|
+
// behaviour already gated on trackTerrainFloor; this just keeps the cache
|
|
630
|
+
// semantics aligned).
|
|
631
|
+
const bool runFloorScan = trackTerrainFloor && !floorScanFromCache;
|
|
632
|
+
|
|
633
|
+
// ── fast path ────────────────────────────────────────────────────────
|
|
634
|
+
// The visible primitive list has been identical for kMaxFramesInFlight
|
|
635
|
+
// frames in a row, so every backend slot is guaranteed to hold a copy of
|
|
636
|
+
// the merged buffers. Skip the merge entirely: re-emit one DrawPrimitive
|
|
637
|
+
// per cached entry with a fresh per-tile MVP and the latest overlay /
|
|
638
|
+
// water-mask state read straight from TileGPUResources. The leftover
|
|
639
|
+
// result.localPositions / altitudes / uvs / indices stay empty — backends
|
|
640
|
+
// detect this via result.geometrySignature and use their cached slot data.
|
|
641
|
+
if (canFastPath) {
|
|
642
|
+
result.draws.reserve(cachedDraws_.size());
|
|
643
|
+
const TileGPUResources* lastScanRes = nullptr;
|
|
644
|
+
for (const auto& c : cachedDraws_) {
|
|
645
|
+
lifecycle_.stampTileUsed(const_cast<TileGPUResources*>(c.res));
|
|
646
|
+
|
|
647
|
+
// Floor scan (run once per res so multi-primitive tiles aren't scanned
|
|
648
|
+
// twice). Skipped when the P5 cache hit above produced a usable value.
|
|
649
|
+
if (runFloorScan && c.res != lastScanRes) {
|
|
650
|
+
lastScanRes = c.res;
|
|
651
|
+
const bool inside =
|
|
652
|
+
(c.wmTileBounds.x <= static_cast<float>(camLonRad) &&
|
|
653
|
+
static_cast<float>(camLonRad) <= c.wmTileBounds.z) &&
|
|
654
|
+
(c.wmTileBounds.y <= static_cast<float>(camLatRad) &&
|
|
655
|
+
static_cast<float>(camLatRad) <= c.wmTileBounds.w);
|
|
656
|
+
if (inside) {
|
|
657
|
+
for (const auto& prim : c.res->primitives) {
|
|
658
|
+
if (prim.altitudes.empty() || prim.localPositions.empty()) continue;
|
|
659
|
+
const size_t vc = prim.localPositions.size();
|
|
660
|
+
for (size_t vi = 0; vi < vc; ++vi) {
|
|
661
|
+
const glm::dvec3 vECEF =
|
|
662
|
+
glm::dvec3(prim.localPositions[vi]) + prim.rtcCenter;
|
|
663
|
+
const double dist = glm::length(vECEF - cameraPos);
|
|
664
|
+
if (dist < bestTerrainDist) {
|
|
665
|
+
bestTerrainDist = dist;
|
|
666
|
+
bestTerrainFloor = prim.altitudes[vi];
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const glm::dvec3 rtcCamRel = c.rtcCenter - glm::dvec3(cameraPos);
|
|
674
|
+
const glm::dmat4 translateD = glm::translate(glm::dmat4(1.0), rtcCamRel);
|
|
675
|
+
|
|
676
|
+
DrawPrimitive draw;
|
|
677
|
+
draw.indexByteOffset = c.indexByteOffset;
|
|
678
|
+
draw.indexCount = c.indexCount;
|
|
679
|
+
draw.hasUVs = c.hasUVs;
|
|
680
|
+
draw.overlayTexture = c.res->overlayTexture;
|
|
681
|
+
draw.overlayTranslation = c.res->overlayTranslation;
|
|
682
|
+
draw.overlayScale = c.res->overlayScale;
|
|
683
|
+
draw.waterMaskTexture = c.res->waterMaskTexture;
|
|
684
|
+
draw.isOnlyWater = c.res->isOnlyWater;
|
|
685
|
+
draw.wmTileBounds = c.wmTileBounds;
|
|
686
|
+
draw.wmTranslation = c.res->wmTranslation;
|
|
687
|
+
draw.wmScale = c.res->wmScale;
|
|
688
|
+
draw.mvpMatrix = glm::mat4(vpDouble * translateD);
|
|
689
|
+
draw.rtcCenterEcef = glm::vec3(c.rtcCenter);
|
|
690
|
+
draw.lodFade = 1.0f;
|
|
691
|
+
result.draws.push_back(draw);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
result.geometrySignature = signature;
|
|
695
|
+
|
|
696
|
+
result.tilesRendered = static_cast<int>(updateResult.tilesToRenderThisFrame.size());
|
|
697
|
+
result.tilesLoading = updateResult.workerThreadTileLoadQueueLength +
|
|
698
|
+
updateResult.mainThreadTileLoadQueueLength;
|
|
699
|
+
result.tilesVisited = static_cast<int>(updateResult.tilesVisited);
|
|
700
|
+
|
|
701
|
+
const auto paramsFP = camera_.getParams();
|
|
702
|
+
result.cameraLat = paramsFP.latitude;
|
|
703
|
+
result.cameraLon = paramsFP.longitude;
|
|
704
|
+
result.cameraAlt = paramsFP.altitude;
|
|
705
|
+
|
|
706
|
+
if (trackTerrainFloor) {
|
|
707
|
+
terrainFloorEllipsoidM_ = bestTerrainFloor;
|
|
708
|
+
// P5: refresh the floor cache on a fresh scan; skip when we hit the
|
|
709
|
+
// cache (the stored value already reflects this signature).
|
|
710
|
+
if (runFloorScan && signature != 0ULL) {
|
|
711
|
+
floorCacheValid_ = true;
|
|
712
|
+
floorCacheSignature_ = signature;
|
|
713
|
+
floorCacheCamLonDeg_ = camParams.longitude;
|
|
714
|
+
floorCacheCamLatDeg_ = camParams.latitude;
|
|
715
|
+
floorCacheValue_ = bestTerrainFloor;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
lifecycle_.advanceFrame();
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// ── Slow path: rebuild cachedDraws_ inline ───────────────────────────────
|
|
724
|
+
// Wipe so the loop below can append. After the loop, if there were no
|
|
725
|
+
// fade-out draws we keep it for the fast path; otherwise we clear and
|
|
726
|
+
// reset the stability counter (the cache is only valid for stable
|
|
727
|
+
// no-fade frames).
|
|
728
|
+
cachedDraws_.clear();
|
|
729
|
+
if (!noVisibleTerrain) cachedDraws_.reserve(updateResult.tilesToRenderThisFrame.size());
|
|
730
|
+
|
|
470
731
|
for (const auto& tile : updateResult.tilesToRenderThisFrame) {
|
|
471
732
|
if (!tile) continue;
|
|
472
733
|
|
|
@@ -492,7 +753,9 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
|
|
|
492
753
|
static_cast<float>(rect.getNorth()));
|
|
493
754
|
|
|
494
755
|
// ── Terrain floor: find nearest vertex to camera nadir ────────────
|
|
495
|
-
|
|
756
|
+
// Suppressed when the cache returned a usable value for the
|
|
757
|
+
// current (signature, camLon, camLat) tuple.
|
|
758
|
+
if (runFloorScan &&
|
|
496
759
|
rect.contains(CesiumGeospatial::Cartographic(camLonRad, camLatRad, 0.0))) {
|
|
497
760
|
for (const auto& prim : res->primitives) {
|
|
498
761
|
if (prim.altitudes.empty() || prim.localPositions.empty()) continue;
|
|
@@ -520,32 +783,47 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
|
|
|
520
783
|
const bool hasPrimUVs =
|
|
521
784
|
!prim.uvs.empty() && prim.uvs.size() == vertexCount;
|
|
522
785
|
|
|
523
|
-
// ── RTC: copy tile-local positions
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
*
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
*altOut++ = prim.altitudes.empty() ? 0.0f : prim.altitudes[vi];
|
|
538
|
-
if (hasPrimUVs) {
|
|
539
|
-
*uvOut++ = prim.uvs[vi].x;
|
|
540
|
-
*uvOut++ = prim.uvs[vi].y;
|
|
541
|
-
} else {
|
|
542
|
-
*uvOut++ = 0.5f;
|
|
543
|
-
*uvOut++ = 0.5f;
|
|
544
|
-
}
|
|
786
|
+
// ── RTC: bulk-copy tile-local positions, altitudes, UVs ─────────────────
|
|
787
|
+
// Use insert(end(), ptr, ptr+n) rather than resize()+memcpy: libc++
|
|
788
|
+
// dispatches insert for trivially-copyable float ranges to memmove
|
|
789
|
+
// (NEON-accelerated even at -O0), skipping the per-element zero-init
|
|
790
|
+
// loop that resize() performs before we'd overwrite the same bytes
|
|
791
|
+
// with memcpy. This halves the memory writes and eliminates the
|
|
792
|
+
// un-vectorised zero-init bottleneck in Debug builds.
|
|
793
|
+
static_assert(sizeof(glm::vec3) == 3 * sizeof(float), "glm::vec3 must be packed");
|
|
794
|
+
static_assert(sizeof(glm::vec2) == 2 * sizeof(float), "glm::vec2 must be packed");
|
|
795
|
+
|
|
796
|
+
{
|
|
797
|
+
const float* posData = reinterpret_cast<const float*>(prim.localPositions.data());
|
|
798
|
+
result.localPositions.insert(result.localPositions.end(),
|
|
799
|
+
posData, posData + vertexCount * 3);
|
|
545
800
|
}
|
|
546
801
|
|
|
547
|
-
|
|
548
|
-
result.
|
|
802
|
+
if (!prim.altitudes.empty()) {
|
|
803
|
+
result.altitudes.insert(result.altitudes.end(),
|
|
804
|
+
prim.altitudes.data(),
|
|
805
|
+
prim.altitudes.data() + vertexCount);
|
|
806
|
+
} else {
|
|
807
|
+
result.altitudes.resize(result.altitudes.size() + vertexCount, 0.0f);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (hasPrimUVs) {
|
|
811
|
+
const float* uvData = reinterpret_cast<const float*>(prim.uvs.data());
|
|
812
|
+
result.uvs.insert(result.uvs.end(), uvData, uvData + vertexCount * 2);
|
|
813
|
+
} else {
|
|
814
|
+
result.uvs.resize(result.uvs.size() + vertexCount * 2, 0.5f);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Indices: add base-vertex offset then insert. Use a temporary vector
|
|
818
|
+
// so the transform writes directly into pre-sized storage.
|
|
819
|
+
{
|
|
820
|
+
const size_t nIdx = prim.indices.size();
|
|
821
|
+
const size_t idxBase = result.indices.size();
|
|
822
|
+
result.indices.resize(idxBase + nIdx);
|
|
823
|
+
const uint32_t bv32 = static_cast<uint32_t>(baseVertex);
|
|
824
|
+
const uint32_t* __restrict srcIdx = prim.indices.data();
|
|
825
|
+
uint32_t* __restrict dstIdx = result.indices.data() + idxBase;
|
|
826
|
+
for (size_t ii = 0; ii < nIdx; ++ii) { dstIdx[ii] = srcIdx[ii] + bv32; }
|
|
549
827
|
}
|
|
550
828
|
|
|
551
829
|
// ── RTC: per-tile MVP matrix (double → float) ─────────────────────────
|
|
@@ -570,14 +848,171 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
|
|
|
570
848
|
draw.wmScale = res->wmScale;
|
|
571
849
|
draw.mvpMatrix = perTileMVP;
|
|
572
850
|
draw.rtcCenterEcef = glm::vec3(prim.rtcCenter);
|
|
851
|
+
// ── LOD fade strategy: incoming tiles are SOLID, outgoing tiles dither ──
|
|
852
|
+
// We deliberately ignore Cesium Native's "fade-in" percentage on the
|
|
853
|
+
// tilesToRenderThisFrame list and always render newly-selected tiles at
|
|
854
|
+
// full opacity. The reason is visibility correctness: if the incoming
|
|
855
|
+
// child were also dithering, both child (fading in) and parent (fading
|
|
856
|
+
// out, see loop below) would simultaneously have holes — and behind the
|
|
857
|
+
// holes is the sky. By rendering the incoming tile fully solid we
|
|
858
|
+
// guarantee a complete opaque "floor" for every screen pixel covered by
|
|
859
|
+
// terrain, and the outgoing tile dithers OUT on top of it. The original
|
|
860
|
+
// ring artefact is still masked because the outgoing tile is the
|
|
861
|
+
// coarser/older surface fading away (its disappearance is what used to
|
|
862
|
+
// be visible as a hard ring).
|
|
863
|
+
draw.lodFade = 1.0f;
|
|
864
|
+
result.draws.push_back(draw);
|
|
865
|
+
|
|
866
|
+
// capture the per-primitive bits the fast path needs to re-emit
|
|
867
|
+
// this draw next frame (camera-dependent MVP is recomputed; everything
|
|
868
|
+
// else is stable as long as the signature is unchanged).
|
|
869
|
+
CachedDraw cd;
|
|
870
|
+
cd.res = res;
|
|
871
|
+
cd.rtcCenter = prim.rtcCenter;
|
|
872
|
+
cd.indexByteOffset = indexByteOff;
|
|
873
|
+
cd.indexCount = static_cast<uint32_t>(prim.indices.size());
|
|
874
|
+
cd.hasUVs = hasPrimUVs;
|
|
875
|
+
cd.wmTileBounds = wmTileBounds;
|
|
876
|
+
cachedDraws_.push_back(cd);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// ── Fading-out tiles (LOD transition) ──────────────────────────────────────
|
|
881
|
+
// The outgoing (de-selected) tiles dither out on top of the already-solid
|
|
882
|
+
// incoming tiles drawn in the loop above. Because the layer behind every
|
|
883
|
+
// dithered pixel is the new solid tile, the dither holes never reveal the
|
|
884
|
+
// sky — eliminating the bleed-through artefacts visible with the previous
|
|
885
|
+
// "both layers fade simultaneously" approach.
|
|
886
|
+
//
|
|
887
|
+
// Cesium Native fade semantics (Tileset.cpp _updateLodTransitions):
|
|
888
|
+
// "We always fade tiles from 0.0 --> 1.0. Whether the tile is fading in or
|
|
889
|
+
// out is determined by whether the tile is in the tilesToRenderThisFrame
|
|
890
|
+
// or tilesFadingOut list."
|
|
891
|
+
//
|
|
892
|
+
// For tilesFadingOut, raw fade goes 0→1 but means "just stopped being
|
|
893
|
+
// selected → fully gone". Our shader treats lodFade as visibility
|
|
894
|
+
// (1=visible, 0=hide), so we invert: visibility = 1 - rawFade.
|
|
895
|
+
// Terrain-floor tracking is skipped (these tiles are being replaced).
|
|
896
|
+
for (const auto& tile : updateResult.tilesFadingOut) {
|
|
897
|
+
if (!tile) continue;
|
|
898
|
+
const auto* renderContent = tile->getContent().getRenderContent();
|
|
899
|
+
if (!renderContent) continue;
|
|
900
|
+
const float fade = 1.0f - renderContent->getLodTransitionFadePercentage();
|
|
901
|
+
if (fade <= 0.0f) continue; // fully faded out — "hide right away" per Cesium Native docs
|
|
902
|
+
|
|
903
|
+
const auto* res = static_cast<const TileGPUResources*>(
|
|
904
|
+
renderContent->getRenderResources());
|
|
905
|
+
if (!res) continue;
|
|
906
|
+
lifecycle_.stampTileUsed(const_cast<TileGPUResources*>(res));
|
|
907
|
+
|
|
908
|
+
glm::vec4 wmTileBounds(0.0f);
|
|
909
|
+
const auto& bv = tile->getBoundingVolume();
|
|
910
|
+
const CesiumGeospatial::BoundingRegion* bRegion =
|
|
911
|
+
Cesium3DTilesSelection::getBoundingRegionFromBoundingVolume(bv);
|
|
912
|
+
if (bRegion) {
|
|
913
|
+
const auto& rect = bRegion->getRectangle();
|
|
914
|
+
wmTileBounds = glm::vec4(
|
|
915
|
+
static_cast<float>(rect.getWest()),
|
|
916
|
+
static_cast<float>(rect.getSouth()),
|
|
917
|
+
static_cast<float>(rect.getEast()),
|
|
918
|
+
static_cast<float>(rect.getNorth()));
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
for (const auto& prim : res->primitives) {
|
|
922
|
+
if (prim.indices.empty() || prim.localPositions.empty()) continue;
|
|
923
|
+
|
|
924
|
+
const size_t baseVertex = result.localPositions.size() / 3;
|
|
925
|
+
const uint32_t indexByteOff =
|
|
926
|
+
static_cast<uint32_t>(result.indices.size() * sizeof(uint32_t));
|
|
927
|
+
const size_t vertexCount = prim.localPositions.size();
|
|
928
|
+
const bool hasPrimUVs =
|
|
929
|
+
!prim.uvs.empty() && prim.uvs.size() == vertexCount;
|
|
930
|
+
|
|
931
|
+
{
|
|
932
|
+
const float* posData = reinterpret_cast<const float*>(prim.localPositions.data());
|
|
933
|
+
result.localPositions.insert(result.localPositions.end(),
|
|
934
|
+
posData, posData + vertexCount * 3);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (!prim.altitudes.empty()) {
|
|
938
|
+
result.altitudes.insert(result.altitudes.end(),
|
|
939
|
+
prim.altitudes.data(),
|
|
940
|
+
prim.altitudes.data() + vertexCount);
|
|
941
|
+
} else {
|
|
942
|
+
result.altitudes.resize(result.altitudes.size() + vertexCount, 0.0f);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (hasPrimUVs) {
|
|
946
|
+
const float* uvData = reinterpret_cast<const float*>(prim.uvs.data());
|
|
947
|
+
result.uvs.insert(result.uvs.end(), uvData, uvData + vertexCount * 2);
|
|
948
|
+
} else {
|
|
949
|
+
result.uvs.resize(result.uvs.size() + vertexCount * 2, 0.5f);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
{
|
|
953
|
+
const size_t nIdx = prim.indices.size();
|
|
954
|
+
const size_t idxBase = result.indices.size();
|
|
955
|
+
result.indices.resize(idxBase + nIdx);
|
|
956
|
+
const uint32_t bv32 = static_cast<uint32_t>(baseVertex);
|
|
957
|
+
const uint32_t* __restrict srcIdx = prim.indices.data();
|
|
958
|
+
uint32_t* __restrict dstIdx = result.indices.data() + idxBase;
|
|
959
|
+
for (size_t ii = 0; ii < nIdx; ++ii) { dstIdx[ii] = srcIdx[ii] + bv32; }
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const glm::dvec3 rtcCamRel =
|
|
963
|
+
prim.rtcCenter - glm::dvec3(cameraPos);
|
|
964
|
+
const glm::dmat4 translateD =
|
|
965
|
+
glm::translate(glm::dmat4(1.0), rtcCamRel);
|
|
966
|
+
const glm::mat4 perTileMVP =
|
|
967
|
+
glm::mat4(vpDouble * translateD);
|
|
968
|
+
|
|
969
|
+
DrawPrimitive draw;
|
|
970
|
+
draw.indexByteOffset = indexByteOff;
|
|
971
|
+
draw.indexCount = static_cast<uint32_t>(prim.indices.size());
|
|
972
|
+
draw.hasUVs = hasPrimUVs;
|
|
973
|
+
draw.overlayTexture = res->overlayTexture;
|
|
974
|
+
draw.overlayTranslation = res->overlayTranslation;
|
|
975
|
+
draw.overlayScale = res->overlayScale;
|
|
976
|
+
draw.waterMaskTexture = res->waterMaskTexture;
|
|
977
|
+
draw.isOnlyWater = res->isOnlyWater;
|
|
978
|
+
draw.wmTileBounds = wmTileBounds;
|
|
979
|
+
draw.wmTranslation = res->wmTranslation;
|
|
980
|
+
draw.wmScale = res->wmScale;
|
|
981
|
+
draw.mvpMatrix = perTileMVP;
|
|
982
|
+
draw.rtcCenterEcef = glm::vec3(prim.rtcCenter);
|
|
983
|
+
draw.lodFade = fade;
|
|
573
984
|
result.draws.push_back(draw);
|
|
574
985
|
}
|
|
575
986
|
}
|
|
576
987
|
|
|
988
|
+
// ── Partition draws so all solid (lodFade==1) tiles render first ────────────
|
|
989
|
+
// Backends use this to bind the solid pipeline first (early-Z enabled on
|
|
990
|
+
// TBDR / Adreno / Mali) and switch to the dither pipeline at most once per
|
|
991
|
+
// frame when the first fading tile is reached. Stable to keep tile order
|
|
992
|
+
// deterministic so identical scenes produce identical command buffers.
|
|
993
|
+
std::stable_partition(result.draws.begin(), result.draws.end(),
|
|
994
|
+
[](const DrawPrimitive& d) { return d.lodFade >= 1.0f; });
|
|
995
|
+
|
|
996
|
+
// ── finalise cache state for next frame ──────────────────────────────
|
|
997
|
+
// Backends consume `geometrySignature` to short-circuit the host→GPU
|
|
998
|
+
// memcpy when their per-slot cached signature matches. We emit a non-zero
|
|
999
|
+
// signature only when this frame is byte-for-byte reproducible from the
|
|
1000
|
+
// cached state — i.e. terrain is present and no LOD fades are in flight.
|
|
1001
|
+
// Any fading-out draw forces a full upload because the dither cutoff
|
|
1002
|
+
// changes every frame.
|
|
1003
|
+
if (anyFadeDraws || noVisibleTerrain) {
|
|
1004
|
+
result.geometrySignature = 0ULL;
|
|
1005
|
+
cachedDraws_.clear();
|
|
1006
|
+
stableSigFrames_ = 0;
|
|
1007
|
+
} else {
|
|
1008
|
+
result.geometrySignature = signature;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
577
1011
|
result.tilesRendered = static_cast<int>(updateResult.tilesToRenderThisFrame.size());
|
|
578
1012
|
result.tilesLoading = updateResult.workerThreadTileLoadQueueLength +
|
|
579
1013
|
updateResult.mainThreadTileLoadQueueLength;
|
|
580
1014
|
result.tilesVisited = static_cast<int>(updateResult.tilesVisited);
|
|
1015
|
+
|
|
581
1016
|
const auto params = camera_.getParams();
|
|
582
1017
|
result.cameraLat = params.latitude;
|
|
583
1018
|
result.cameraLon = params.longitude;
|
|
@@ -585,6 +1020,24 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
|
|
|
585
1020
|
|
|
586
1021
|
if (trackTerrainFloor) {
|
|
587
1022
|
terrainFloorEllipsoidM_ = bestTerrainFloor;
|
|
1023
|
+
// refresh the floor cache on a fresh scan (slow path always runs a
|
|
1024
|
+
// scan unless the cache short-circuit fired above). Only memoise when
|
|
1025
|
+
// the geometry signature is stable enough to key on.
|
|
1026
|
+
if (runFloorScan && signature != 0ULL) {
|
|
1027
|
+
floorCacheValid_ = true;
|
|
1028
|
+
floorCacheSignature_ = signature;
|
|
1029
|
+
floorCacheCamLonDeg_ = camParams.longitude;
|
|
1030
|
+
floorCacheCamLatDeg_ = camParams.latitude;
|
|
1031
|
+
floorCacheValue_ = bestTerrainFloor;
|
|
1032
|
+
} else if (!runFloorScan) {
|
|
1033
|
+
// Cache hit on the slow path is unusual but possible (e.g. fade
|
|
1034
|
+
// started this frame after a stable cache was warm). Nothing to do
|
|
1035
|
+
// — terrainFloorEllipsoidM_ is already the cached value.
|
|
1036
|
+
} else {
|
|
1037
|
+
// Cannot key on signature=0; invalidate so the next stable frame
|
|
1038
|
+
// re-populates the cache deterministically.
|
|
1039
|
+
floorCacheValid_ = false;
|
|
1040
|
+
}
|
|
588
1041
|
}
|
|
589
1042
|
|
|
590
1043
|
lifecycle_.advanceFrame();
|