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.
@@ -24,24 +24,39 @@ set(SHADER_DIR "${CMAKE_SOURCE_DIR}/src/main/cpp/vulkan/shaders")
24
24
  set(SPIRV_OUTPUT_DIR "${CMAKE_BINARY_DIR}/shaders")
25
25
  file(MAKE_DIRECTORY "${SPIRV_OUTPUT_DIR}")
26
26
 
27
- set(SHADER_SOURCES
28
- "${SHADER_DIR}/terrain.vert"
29
- "${SHADER_DIR}/terrain.frag"
30
- "${SHADER_DIR}/sky.vert"
31
- "${SHADER_DIR}/sky.frag"
27
+ # Each entry: "SOURCE|OUTPUT_BASENAME|GLSLC_DEFINES"
28
+ # terrain.frag is built TWICE — once plain (solid pipeline, no discard so
29
+ # early-Z stays enabled) and once with TERRAIN_DITHER=1 (dither pipeline used
30
+ # only for tiles actively crossing an LOD transition). See VulkanBackend
31
+ # createTerrain{Solid,Dither}Pipeline for how the two SPV blobs are consumed.
32
+ set(SHADER_BUILDS
33
+ "${SHADER_DIR}/terrain.vert|terrain.vert|"
34
+ "${SHADER_DIR}/terrain.frag|terrain.frag|"
35
+ "${SHADER_DIR}/terrain.frag|terrain_dither.frag|TERRAIN_DITHER=1"
36
+ "${SHADER_DIR}/sky.vert|sky.vert|"
37
+ "${SHADER_DIR}/sky.frag|sky.frag|"
32
38
  )
33
39
 
34
- foreach(SHADER ${SHADER_SOURCES})
35
- get_filename_component(SHADER_NAME ${SHADER} NAME)
40
+ foreach(BUILD ${SHADER_BUILDS})
41
+ string(REPLACE "|" ";" PARTS "${BUILD}")
42
+ list(GET PARTS 0 SHADER)
43
+ list(GET PARTS 1 SHADER_NAME)
44
+ list(GET PARTS 2 SHADER_DEFINES)
45
+
36
46
  set(SPV_FILE "${SPIRV_OUTPUT_DIR}/${SHADER_NAME}.spv")
37
47
  set(SPV_HEADER "${SPIRV_OUTPUT_DIR}/${SHADER_NAME}.spv.h")
38
48
  string(REGEX REPLACE "[^a-zA-Z0-9]" "_" ARRAY_NAME "spv_${SHADER_NAME}")
39
49
 
50
+ set(GLSLC_ARGS -O)
51
+ if (SHADER_DEFINES)
52
+ set(GLSLC_ARGS ${GLSLC_ARGS} "-D${SHADER_DEFINES}")
53
+ endif()
54
+
40
55
  add_custom_command(
41
56
  OUTPUT "${SPV_FILE}"
42
- COMMAND "${GLSLC}" -O -o "${SPV_FILE}" "${SHADER}"
57
+ COMMAND "${GLSLC}" ${GLSLC_ARGS} -o "${SPV_FILE}" "${SHADER}"
43
58
  DEPENDS "${SHADER}"
44
- COMMENT "Compiling ${SHADER_NAME} -> SPIR-V"
59
+ COMMENT "Compiling ${SHADER_NAME} -> SPIR-V (defines: ${SHADER_DEFINES})"
45
60
  )
46
61
 
47
62
  # Generate a C header embedding the SPIR-V as a uint32_t array
@@ -50,6 +50,7 @@
50
50
  // Pre-compiled SPIR-V headers (generated at build time by glslc + spv_to_header.cmake)
51
51
  #include "terrain.vert.spv.h"
52
52
  #include "terrain.frag.spv.h"
53
+ #include "terrain_dither.frag.spv.h"
53
54
  #include "sky.vert.spv.h"
54
55
  #include "sky.frag.spv.h"
55
56
 
@@ -109,7 +110,7 @@ struct TerrainFragmentPC {
109
110
  float translationY; // 4 bytes
110
111
  float scaleX; // 4 bytes
111
112
  float scaleY; // 4 bytes
112
- float _pad; // 4 bytes
113
+ float lodFade; // 4 bytes — LOD dither threshold
113
114
  }; // 64 bytes
114
115
 
115
116
  namespace reactnativecesium {
@@ -296,6 +297,10 @@ void VulkanBackend::shutdown() {
296
297
  }
297
298
 
298
299
  for (int i = 0; i < kMaxFramesInFlight; ++i) {
300
+ if (vtxMapped_[i]) { vkUnmapMemory(device_, vtxMems_[i]); vtxMapped_[i] = nullptr; }
301
+ if (idxMapped_[i]) { vkUnmapMemory(device_, idxMems_[i]); idxMapped_[i] = nullptr; }
302
+ if (uvMapped_[i]) { vkUnmapMemory(device_, uvMems_[i]); uvMapped_[i] = nullptr; }
303
+ if (altMapped_[i]) { vkUnmapMemory(device_, altMems_[i]); altMapped_[i] = nullptr; }
299
304
  if (vtxBufs_[i]) vkDestroyBuffer(device_, vtxBufs_[i], nullptr);
300
305
  if (vtxMems_[i]) vkFreeMemory(device_, vtxMems_[i], nullptr);
301
306
  if (idxBufs_[i]) vkDestroyBuffer(device_, idxBufs_[i], nullptr);
@@ -858,7 +863,7 @@ void VulkanBackend::createSyncObjects() {
858
863
  // renderFinishedSemaphores_ created in beginFrame after swapchain is ready
859
864
  }
860
865
 
861
- // ── Pipeline cache (P1-vk-pipeline-cache) ───────────────────────────────────────
866
+ // ── Pipeline cache (vk-pipeline-cache) ───────────────────────────────────────
862
867
 
863
868
  namespace {
864
869
  std::string pipelineCachePath(const std::string& cacheDir) {
@@ -1173,9 +1178,6 @@ void VulkanBackend::createSkyPipeline() {
1173
1178
  }
1174
1179
 
1175
1180
  void VulkanBackend::createTerrainPipeline() {
1176
- VkShaderModule vertModule = createShaderModule(device_, spv_terrain_vert, spv_terrain_vert_size);
1177
- VkShaderModule fragModule = createShaderModule(device_, spv_terrain_frag, spv_terrain_frag_size);
1178
-
1179
1181
  // Vertex bindings: position (vec3) | uv (vec2) | altitude (float).
1180
1182
  static const VkVertexInputBindingDescription bindings[3] = {
1181
1183
  {0, 3 * sizeof(float), VK_VERTEX_INPUT_RATE_VERTEX},
@@ -1188,22 +1190,41 @@ void VulkanBackend::createTerrainPipeline() {
1188
1190
  {2, 2, VK_FORMAT_R32_SFLOAT, 0},
1189
1191
  };
1190
1192
 
1191
- PipelineBuildSpec spec{};
1192
- spec.vert = vertModule;
1193
- spec.frag = fragModule;
1194
- spec.bindingCount = 3;
1195
- spec.bindings = bindings;
1196
- spec.attributeCount = 3;
1197
- spec.attributes = attrs;
1198
- spec.depthTest = true;
1199
- spec.depthWrite = true;
1200
- spec.cull = VK_CULL_MODE_BACK_BIT;
1201
- spec.samples = sampleCount_;
1202
- terrainPipeline_ = buildPipeline(device_, terrainPipelineLayout_, renderPass_,
1203
- pipelineCache_, spec);
1193
+ VkShaderModule vertModule = createShaderModule(
1194
+ device_, spv_terrain_vert, spv_terrain_vert_size);
1195
+ VkShaderModule fragSolidModule = createShaderModule(
1196
+ device_, spv_terrain_frag, spv_terrain_frag_size);
1197
+ VkShaderModule fragDitherModule = createShaderModule(
1198
+ device_, spv_terrain_dither_frag, spv_terrain_dither_frag_size);
1199
+
1200
+ auto buildOne = [&](VkShaderModule fragModule) {
1201
+ PipelineBuildSpec spec{};
1202
+ spec.vert = vertModule;
1203
+ spec.frag = fragModule;
1204
+ spec.bindingCount = 3;
1205
+ spec.bindings = bindings;
1206
+ spec.attributeCount = 3;
1207
+ spec.attributes = attrs;
1208
+ spec.depthTest = true;
1209
+ spec.depthWrite = true;
1210
+ spec.cull = VK_CULL_MODE_BACK_BIT;
1211
+ spec.samples = sampleCount_;
1212
+ return buildPipeline(device_, terrainPipelineLayout_, renderPass_,
1213
+ pipelineCache_, spec);
1214
+ };
1204
1215
 
1205
- vkDestroyShaderModule(device_, vertModule, nullptr);
1206
- vkDestroyShaderModule(device_, fragModule, nullptr);
1216
+ // Solid pipeline: no LOD-fade discard in the fragment shader, so the GPU's
1217
+ // early-Z optimisation runs at full speed. This is the pipeline used for
1218
+ // the overwhelming majority of draws outside an active LOD transition.
1219
+ terrainSolidPipeline_ = buildOne(fragSolidModule);
1220
+ // Dither pipeline: includes IGN-based discard. Used only for draws where
1221
+ // DrawPrimitive::lodFade < 1.0f (see CesiumEngine::updateFrame which
1222
+ // partitions draws so the switch happens at most once per frame).
1223
+ terrainDitherPipeline_ = buildOne(fragDitherModule);
1224
+
1225
+ vkDestroyShaderModule(device_, vertModule, nullptr);
1226
+ vkDestroyShaderModule(device_, fragSolidModule, nullptr);
1227
+ vkDestroyShaderModule(device_, fragDitherModule, nullptr);
1207
1228
  }
1208
1229
 
1209
1230
  void VulkanBackend::createGraphicsPipelinesAll() {
@@ -1212,9 +1233,12 @@ void VulkanBackend::createGraphicsPipelinesAll() {
1212
1233
  }
1213
1234
 
1214
1235
  void VulkanBackend::destroyGraphicsPipelinesAll() {
1215
- if (terrainPipeline_) vkDestroyPipeline(device_, terrainPipeline_, nullptr);
1216
- if (skyPipeline_) vkDestroyPipeline(device_, skyPipeline_, nullptr);
1217
- terrainPipeline_ = skyPipeline_ = VK_NULL_HANDLE;
1236
+ if (terrainSolidPipeline_) vkDestroyPipeline(device_, terrainSolidPipeline_, nullptr);
1237
+ if (terrainDitherPipeline_) vkDestroyPipeline(device_, terrainDitherPipeline_, nullptr);
1238
+ if (skyPipeline_) vkDestroyPipeline(device_, skyPipeline_, nullptr);
1239
+ terrainSolidPipeline_ = VK_NULL_HANDLE;
1240
+ terrainDitherPipeline_ = VK_NULL_HANDLE;
1241
+ skyPipeline_ = VK_NULL_HANDLE;
1218
1242
  }
1219
1243
 
1220
1244
  // ── Memory / buffer helpers ─────────────────────────────────────────────────────
@@ -1285,23 +1309,29 @@ bool VulkanBackend::createBuffer(VkDeviceSize size, VkBufferUsageFlags usage,
1285
1309
  }
1286
1310
 
1287
1311
  void VulkanBackend::ensureBuffer(VkBuffer& buffer, VkDeviceMemory& memory,
1288
- size_t& capacity, size_t needed,
1312
+ void*& mapped, size_t& capacity, size_t needed,
1289
1313
  const void* data, VkBufferUsageFlags usage) {
1290
1314
  if (needed == 0) return;
1291
1315
  if (needed > capacity) {
1316
+ if (mapped) { vkUnmapMemory(device_, memory); mapped = nullptr; }
1292
1317
  if (buffer) vkDestroyBuffer(device_, buffer, nullptr);
1293
1318
  if (memory) vkFreeMemory(device_, memory, nullptr);
1294
1319
  size_t newCap = std::max(needed, capacity > 0 ? capacity * 2 : needed);
1295
1320
  newCap = (newCap + 4095UL) & ~4095UL;
1296
- createBuffer(static_cast<VkDeviceSize>(newCap), usage,
1297
- VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
1298
- buffer, memory);
1321
+ if (!createBuffer(static_cast<VkDeviceSize>(newCap), usage,
1322
+ VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
1323
+ buffer, memory)) {
1324
+ capacity = 0;
1325
+ return;
1326
+ }
1299
1327
  capacity = newCap;
1328
+ if (vkMapMemory(device_, memory, 0, VK_WHOLE_SIZE, 0, &mapped) != VK_SUCCESS) {
1329
+ mapped = nullptr;
1330
+ return;
1331
+ }
1300
1332
  }
1301
- void* mapped = nullptr;
1302
- if (vkMapMemory(device_, memory, 0, needed, 0, &mapped) != VK_SUCCESS) return;
1333
+ if (!mapped) return;
1303
1334
  memcpy(mapped, data, needed);
1304
- vkUnmapMemory(device_, memory);
1305
1335
  }
1306
1336
 
1307
1337
  VkCommandBuffer VulkanBackend::beginSingleTimeCommands() {
@@ -1449,25 +1479,34 @@ void VulkanBackend::initStagingRing(VkDeviceSize bytes) {
1449
1479
  stagingMemory_ = VK_NULL_HANDLE;
1450
1480
  return;
1451
1481
  }
1452
- stagingMapped_ = static_cast<uint8_t*>(mapped);
1453
- stagingHead_ = 0;
1482
+ stagingMapped_ = static_cast<uint8_t*>(mapped);
1483
+ stagingHead_ = 0;
1484
+ stagingBytesThisBatch_ = 0;
1454
1485
  }
1455
1486
 
1456
1487
  VkDeviceSize VulkanBackend::allocateStagingSlice(VkDeviceSize size) {
1457
1488
  if (!stagingMapped_ || size == 0 || size > kStagingSize) return SIZE_MAX;
1458
1489
 
1459
1490
  VkDeviceSize aligned = (size + kStagingAlignment - 1) & ~(kStagingAlignment - 1);
1491
+
1492
+ // per-batch budget. Refuse the allocation if granting it would let a
1493
+ // single beginFrame->flushPendingUploads cycle eat more than 1/N of the
1494
+ // ring. The caller (createImageFromPixels) falls back to the synchronous
1495
+ // single-tile path for that tile so the rest of the batch still goes via
1496
+ // the async ring.
1497
+ if (stagingBytesThisBatch_ + aligned > kStagingPerFrameBudget) {
1498
+ return SIZE_MAX;
1499
+ }
1500
+
1460
1501
  VkDeviceSize off;
1461
1502
  if (stagingHead_ + aligned > kStagingSize) {
1462
- // Wrap to the start of the ring. Anything still in flight from before the
1463
- // wrap will keep its previous offset alive until its upload fence
1464
- // signalled in beginFrame.
1465
1503
  off = 0;
1466
1504
  stagingHead_ = aligned;
1467
1505
  } else {
1468
1506
  off = stagingHead_;
1469
1507
  stagingHead_ += aligned;
1470
1508
  }
1509
+ stagingBytesThisBatch_ += aligned;
1471
1510
  return off;
1472
1511
  }
1473
1512
 
@@ -1486,7 +1525,7 @@ void VulkanBackend::flushPendingUploads() {
1486
1525
  LOGE("flushPendingUploads: upload command buffer is NULL!");
1487
1526
  return;
1488
1527
  }
1489
-
1528
+
1490
1529
  vkResetCommandBuffer(cmd, 0);
1491
1530
 
1492
1531
  VkCommandBufferBeginInfo bi{};
@@ -1506,7 +1545,7 @@ void VulkanBackend::flushPendingUploads() {
1506
1545
  safeMipLevels = 1;
1507
1546
  u.tex->mipLevels = 1;
1508
1547
  }
1509
-
1548
+
1510
1549
  VkImageMemoryBarrier b{};
1511
1550
  b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
1512
1551
  b.srcAccessMask = 0;
@@ -1574,23 +1613,41 @@ void VulkanBackend::flushPendingUploads() {
1574
1613
 
1575
1614
  vkEndCommandBuffer(cmd);
1576
1615
 
1577
- // SYNCHRONOUS submit and wait - simpler and more reliable than fence-based
1578
- // batching. The performance hit is minimal since uploads are infrequent.
1616
+ // async submit. The fence lets beginFrame reclaim the staging slices in
1617
+ // kMaxFramesInFlight frames' time without ever calling vkQueueWaitIdle.
1618
+ // Ordering: this submit precedes endFrame()'s draw submit on the same
1619
+ // graphicsQueue_, so the image layout transitions recorded above are
1620
+ // visible to subsequent fragment-shader reads via the standard in-queue
1621
+ // execution + memory-barrier rules — no extra semaphore needed.
1622
+ vkResetFences(device_, 1, &uploadFences_[frameIndex_]);
1623
+
1579
1624
  VkSubmitInfo si{};
1580
1625
  si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
1581
1626
  si.commandBufferCount = 1;
1582
1627
  si.pCommandBuffers = &cmd;
1583
-
1584
- VkResult submitResult = vkQueueSubmit(graphicsQueue_, 1, &si, VK_NULL_HANDLE);
1628
+
1629
+ VkResult submitResult =
1630
+ vkQueueSubmit(graphicsQueue_, 1, &si, uploadFences_[frameIndex_]);
1585
1631
  if (submitResult != VK_SUCCESS) {
1586
1632
  LOGE("flushPendingUploads: vkQueueSubmit failed: %d", submitResult);
1587
1633
  return;
1588
1634
  }
1589
-
1590
- vkQueueWaitIdle(graphicsQueue_);
1591
1635
 
1592
1636
  // Mark each texture as visible to subsequent draws (no longer pending).
1637
+ // Safe to flip here because the next draw submit happens after this one on
1638
+ // the same queue, and the upload CB ends in a TRANSFER → FRAGMENT_SHADER
1639
+ // memory barrier.
1593
1640
  for (auto& u : uploads) u.tex->pendingUpload = false;
1641
+
1642
+ // Move uploads into this slot's in-flight list so beginFrame can free
1643
+ // their staging slices once uploadFences_[frameIndex_] is signalled (which
1644
+ // happens at most kMaxFramesInFlight frames from now).
1645
+ {
1646
+ std::lock_guard<std::mutex> lk(stagingMutex_);
1647
+ auto& inflight = uploadsInFlight_[frameIndex_];
1648
+ inflight.insert(inflight.end(), uploads.begin(), uploads.end());
1649
+ stagingBytesThisBatch_ = 0;
1650
+ }
1594
1651
  }
1595
1652
 
1596
1653
  // ── Generic image creator (P2-pipelines-dedupe) ─────────────────────────────
@@ -1898,13 +1955,13 @@ void VulkanBackend::recreateSwapchain() {
1898
1955
 
1899
1956
  void VulkanBackend::beginFrame(const FrameParams& /*params*/) {
1900
1957
  if (device_ == VK_NULL_HANDLE) return;
1901
-
1958
+
1902
1959
  // On Android, defer swapchain creation until the first frame to ensure the
1903
1960
  // surface compositor is fully ready. Creating it too early can cause
1904
1961
  // vkAcquireNextImageKHR to block indefinitely.
1905
1962
  if (swapchain_ == VK_NULL_HANDLE) {
1906
1963
  createSwapchain();
1907
-
1964
+
1908
1965
  // Create renderFinishedSemaphores now that we know swapchainImages_.size()
1909
1966
  renderFinishedSemaphores_.resize(swapchainImages_.size());
1910
1967
  VkSemaphoreCreateInfo semInfo{};
@@ -1912,7 +1969,7 @@ void VulkanBackend::beginFrame(const FrameParams& /*params*/) {
1912
1969
  for (size_t i = 0; i < renderFinishedSemaphores_.size(); ++i) {
1913
1970
  VK_CHECK(vkCreateSemaphore(device_, &semInfo, nullptr, &renderFinishedSemaphores_[i]));
1914
1971
  }
1915
-
1972
+
1916
1973
  createRenderPass();
1917
1974
  createDepthResources();
1918
1975
  if (sampleCount_ != VK_SAMPLE_COUNT_1_BIT) createMsaaResources();
@@ -1958,11 +2015,14 @@ void VulkanBackend::beginFrame(const FrameParams& /*params*/) {
1958
2015
  ++totalFrameCount_;
1959
2016
  flushPendingDeletes();
1960
2017
 
1961
- // Reclaim staging slices for uploads that have now finished on the GPU.
1962
- // (Their submit was made N=kMaxFramesInFlight frames ago against
1963
- // uploadFences_[frameIndex_]; the wait above is on inFlightFences_, but
1964
- // since uploads were submitted on the same queue *before* the previous
1965
- // frame's draw they must have completed too.)
2018
+ // reclaim staging slices owned by this slot's previous async upload
2019
+ // submit. The upload fence is created SIGNALED, so the first wait through
2020
+ // each slot is a no-op; on subsequent passes it ensures the GPU has finished
2021
+ // reading those staging bytes before we let allocateStagingSlice hand them
2022
+ // back out.
2023
+ if (uploadFences_[frameIndex_] != VK_NULL_HANDLE) {
2024
+ vkWaitForFences(device_, 1, &uploadFences_[frameIndex_], VK_TRUE, UINT64_MAX);
2025
+ }
1966
2026
  {
1967
2027
  std::lock_guard<std::mutex> lk(stagingMutex_);
1968
2028
  uploadsInFlight_[frameIndex_].clear();
@@ -1976,7 +2036,7 @@ void VulkanBackend::beginFrame(const FrameParams& /*params*/) {
1976
2036
  VkResult result = vkAcquireNextImageKHR(device_, swapchain_, 1000000000ULL,
1977
2037
  imageAvailableSemaphores_[frameIndex_],
1978
2038
  VK_NULL_HANDLE, &imageIndex_);
1979
-
2039
+
1980
2040
  if (result == VK_TIMEOUT) {
1981
2041
  LOGE("VulkanBackend::beginFrame: vkAcquireNextImageKHR timed out");
1982
2042
  frameBegan_ = false;
@@ -2065,26 +2125,48 @@ void VulkanBackend::drawScene(const FrameResult& frame) {
2065
2125
  vkCmdDraw(cmd, 3, 1, 0, 0);
2066
2126
  }
2067
2127
 
2068
- if (frame.draws.empty() || frame.localPositions.empty() || frame.indices.empty() ||
2069
- frame.altitudes.empty() || !terrainPipeline_) {
2128
+ if (frame.draws.empty() || !terrainSolidPipeline_ || !terrainDitherPipeline_) {
2070
2129
  return;
2071
2130
  }
2072
2131
 
2073
2132
  const int fi = frameIndex_;
2074
2133
 
2075
- const size_t vtxBytes = frame.localPositions.size() * sizeof(float);
2076
- const size_t idxBytes = frame.indices.size() * sizeof(uint32_t);
2077
- const size_t uvBytes = frame.uvs.size() * sizeof(float);
2078
- const size_t altBytes = frame.altitudes.size() * sizeof(float);
2134
+ // check the per-slot geometry signature cache. When it matches we
2135
+ // know the persistent vtx/idx/uv/alt buffers already hold the right bytes
2136
+ // and the engine intentionally left the merged CPU arrays empty.
2137
+ const uint64_t sig = frame.geometrySignature;
2138
+ const bool slotMatches = (sig != 0ULL) && (sig == geomSlotSig_[fi]);
2079
2139
 
2080
- ensureBuffer(vtxBufs_[fi], vtxMems_[fi], vtxCaps_[fi], vtxBytes,
2081
- frame.localPositions.data(), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
2082
- ensureBuffer(idxBufs_[fi], idxMems_[fi], idxCaps_[fi], idxBytes,
2083
- frame.indices.data(), VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
2084
- ensureBuffer(uvBufs_[fi], uvMems_[fi], uvCaps_[fi], uvBytes,
2085
- frame.uvs.data(), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
2086
- ensureBuffer(altBufs_[fi], altMems_[fi], altCaps_[fi], altBytes,
2087
- frame.altitudes.data(), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
2140
+ if (!slotMatches && (frame.localPositions.empty() || frame.indices.empty() ||
2141
+ frame.altitudes.empty())) {
2142
+ return;
2143
+ }
2144
+
2145
+ if (!slotMatches) {
2146
+ const size_t vtxBytes = frame.localPositions.size() * sizeof(float);
2147
+ const size_t idxBytes = frame.indices.size() * sizeof(uint32_t);
2148
+ const size_t uvBytes = frame.uvs.size() * sizeof(float);
2149
+ const size_t altBytes = frame.altitudes.size() * sizeof(float);
2150
+
2151
+ ensureBuffer(vtxBufs_[fi], vtxMems_[fi], vtxMapped_[fi], vtxCaps_[fi],
2152
+ vtxBytes, frame.localPositions.data(),
2153
+ VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
2154
+ ensureBuffer(idxBufs_[fi], idxMems_[fi], idxMapped_[fi], idxCaps_[fi],
2155
+ idxBytes, frame.indices.data(),
2156
+ VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
2157
+ ensureBuffer(uvBufs_[fi], uvMems_[fi], uvMapped_[fi], uvCaps_[fi],
2158
+ uvBytes, frame.uvs.data(),
2159
+ VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
2160
+ ensureBuffer(altBufs_[fi], altMems_[fi], altMapped_[fi], altCaps_[fi],
2161
+ altBytes, frame.altitudes.data(),
2162
+ VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
2163
+
2164
+ // Record what this slot now holds — set only after every buffer has
2165
+ // been refreshed. If `sig == 0` we deliberately propagate it so future
2166
+ // frames re-upload on this slot (e.g. an active fade is invalidating
2167
+ // the cache).
2168
+ geomSlotSig_[fi] = sig;
2169
+ }
2088
2170
 
2089
2171
  if (!vtxBufs_[fi] || !idxBufs_[fi]) return;
2090
2172
 
@@ -2095,7 +2177,11 @@ void VulkanBackend::drawScene(const FrameResult& frame) {
2095
2177
  terrU.cameraEcef[3] = 0.0f;
2096
2178
  memcpy(terrainUboMapped_[fi], &terrU, sizeof(TerrainUBO));
2097
2179
 
2098
- vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, terrainPipeline_);
2180
+ // Start with the solid pipeline. CesiumEngine has stable_partition-ed draws so
2181
+ // every lodFade==1 draw is first; we only switch to the dither pipeline once,
2182
+ // when the first fading draw is reached.
2183
+ VkPipeline currentPipeline = terrainSolidPipeline_;
2184
+ vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, currentPipeline);
2099
2185
 
2100
2186
  VkBuffer vertexBuffers[] = {vtxBufs_[fi], uvBufs_[fi], altBufs_[fi]};
2101
2187
  VkDeviceSize offsets[] = {0, 0, 0};
@@ -2125,6 +2211,18 @@ void VulkanBackend::drawScene(const FrameResult& frame) {
2125
2211
  for (const auto& draw : frame.draws) {
2126
2212
  if (draw.indexCount == 0) continue;
2127
2213
 
2214
+ // ── Pipeline switch for LOD-fade dither ─────────────────────────────────
2215
+ // Solid pipeline (no discard) for fully-visible tiles keeps Adreno/Mali
2216
+ // early-Z enabled at full speed; dither pipeline only used for actively-
2217
+ // fading tiles. Draws are partitioned so this switch fires at most once
2218
+ // per frame.
2219
+ VkPipeline wantedPipeline = (draw.lodFade < 1.0f)
2220
+ ? terrainDitherPipeline_ : terrainSolidPipeline_;
2221
+ if (wantedPipeline != currentPipeline) {
2222
+ currentPipeline = wantedPipeline;
2223
+ vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, currentPipeline);
2224
+ }
2225
+
2128
2226
  const float* mp = glm::value_ptr(draw.mvpMatrix);
2129
2227
  for (int i = 0; i < 16; ++i) pc.v.mvpMatrix[i] = mp[i];
2130
2228
 
@@ -2143,6 +2241,7 @@ void VulkanBackend::drawScene(const FrameResult& frame) {
2143
2241
  pc.f.translationY = draw.overlayTranslation.y;
2144
2242
  pc.f.scaleX = draw.overlayScale.x;
2145
2243
  pc.f.scaleY = draw.overlayScale.y;
2244
+ pc.f.lodFade = draw.lodFade;
2146
2245
  vkCmdPushConstants(cmd, terrainPipelineLayout_,
2147
2246
  VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
2148
2247
  0, sizeof(CombinedPC), &pc);
@@ -104,8 +104,14 @@ private:
104
104
  bool createBuffer(VkDeviceSize size, VkBufferUsageFlags usage,
105
105
  VkMemoryPropertyFlags props, VkBuffer& buffer,
106
106
  VkDeviceMemory& memory);
107
- void ensureBuffer(VkBuffer& buffer, VkDeviceMemory& memory, size_t& capacity,
108
- size_t needed, const void* data, VkBufferUsageFlags usage);
107
+ // Grow `buffer`/`memory` if needed and copy `data` (size = needed bytes)
108
+ // into it via the persistently mapped pointer (`mapped`). On reallocation
109
+ // the previous mapping is unmapped and `mapped` is rewritten with the new
110
+ // map; otherwise the existing map is reused, saving a map/unmap pair per
111
+ // frame for each of the four merged terrain buffers.
112
+ void ensureBuffer(VkBuffer& buffer, VkDeviceMemory& memory, void*& mapped,
113
+ size_t& capacity, size_t needed, const void* data,
114
+ VkBufferUsageFlags usage);
109
115
 
110
116
  // ── Deferred upload pipeline ─────────────────────────────────────────────
111
117
  // Each call to createRasterTexture / createWaterMaskTexture allocates a slice
@@ -198,7 +204,12 @@ private:
198
204
  VkDescriptorSetLayout skyDescSetLayout_ = VK_NULL_HANDLE;
199
205
  VkPipelineLayout terrainPipelineLayout_ = VK_NULL_HANDLE;
200
206
  VkPipelineLayout skyPipelineLayout_ = VK_NULL_HANDLE;
201
- VkPipeline terrainPipeline_ = VK_NULL_HANDLE;
207
+ // Two terrain pipelines built from the same GLSL source (compiled twice via
208
+ // -DTERRAIN_DITHER=1 in android/CMakeLists.txt). The solid pipeline omits
209
+ // the LOD-fade discard so early-Z stays enabled at full speed on Adreno/Mali;
210
+ // the dither pipeline is used only for draws where DrawPrimitive::lodFade<1.
211
+ VkPipeline terrainSolidPipeline_ = VK_NULL_HANDLE;
212
+ VkPipeline terrainDitherPipeline_ = VK_NULL_HANDLE;
202
213
  VkPipeline skyPipeline_ = VK_NULL_HANDLE;
203
214
 
204
215
  VkDescriptorPool descriptorPool_ = VK_NULL_HANDLE;
@@ -234,22 +245,38 @@ private:
234
245
  uint32_t imageIndex_ = 0;
235
246
  bool frameBegan_ = false;
236
247
 
237
- // Triple-buffered persistent vertex/index/UV/altitude buffers
238
- VkBuffer vtxBufs_[kMaxFramesInFlight] = {};
239
- VkDeviceMemory vtxMems_[kMaxFramesInFlight] = {};
240
- size_t vtxCaps_[kMaxFramesInFlight] = {};
241
-
242
- VkBuffer idxBufs_[kMaxFramesInFlight] = {};
243
- VkDeviceMemory idxMems_[kMaxFramesInFlight] = {};
244
- size_t idxCaps_[kMaxFramesInFlight] = {};
245
-
246
- VkBuffer uvBufs_[kMaxFramesInFlight] = {};
247
- VkDeviceMemory uvMems_[kMaxFramesInFlight] = {};
248
- size_t uvCaps_[kMaxFramesInFlight] = {};
249
-
250
- VkBuffer altBufs_[kMaxFramesInFlight] = {}; // float altitude per vertex
251
- VkDeviceMemory altMems_[kMaxFramesInFlight] = {};
252
- size_t altCaps_[kMaxFramesInFlight] = {};
248
+ // Triple-buffered persistent vertex/index/UV/altitude buffers.
249
+ // The mapped pointers are kept alive for the lifetime of the buffer (only
250
+ // unmapped when the underlying memory is freed on reallocation/shutdown),
251
+ // mirroring how skyUboMapped_ / terrainUboMapped_ are handled — this saves
252
+ // four map/unmap pairs per frame.
253
+ VkBuffer vtxBufs_[kMaxFramesInFlight] = {};
254
+ VkDeviceMemory vtxMems_[kMaxFramesInFlight] = {};
255
+ void* vtxMapped_[kMaxFramesInFlight] = {};
256
+ size_t vtxCaps_[kMaxFramesInFlight] = {};
257
+
258
+ VkBuffer idxBufs_[kMaxFramesInFlight] = {};
259
+ VkDeviceMemory idxMems_[kMaxFramesInFlight] = {};
260
+ void* idxMapped_[kMaxFramesInFlight] = {};
261
+ size_t idxCaps_[kMaxFramesInFlight] = {};
262
+
263
+ VkBuffer uvBufs_[kMaxFramesInFlight] = {};
264
+ VkDeviceMemory uvMems_[kMaxFramesInFlight] = {};
265
+ void* uvMapped_[kMaxFramesInFlight] = {};
266
+ size_t uvCaps_[kMaxFramesInFlight] = {};
267
+
268
+ VkBuffer altBufs_[kMaxFramesInFlight] = {}; // float altitude per vertex
269
+ VkDeviceMemory altMems_[kMaxFramesInFlight] = {};
270
+ void* altMapped_[kMaxFramesInFlight] = {};
271
+ size_t altCaps_[kMaxFramesInFlight] = {};
272
+
273
+ // ── per-slot geometry signature cache ────────────────────────────────
274
+ // When FrameResult::geometrySignature matches the value last written into
275
+ // this slot, the four merged buffers above already contain exactly the
276
+ // bytes the engine would have produced this frame and the host memcpys can
277
+ // be skipped. 0 means "this slot is stale" — backends should always
278
+ // memcpy when geomSlotSig_[fi] is 0 or differs from the incoming sig.
279
+ uint64_t geomSlotSig_[kMaxFramesInFlight] = {};
253
280
 
254
281
  // Per-frame UBO: just cameraEcef (fragment stage, used for lighting vd).
255
282
  // MVP is now per-draw via push constants.
@@ -270,10 +297,24 @@ private:
270
297
  // beginFrame when the matching upload fence is signalled).
271
298
  static constexpr VkDeviceSize kStagingSize = 64ULL * 1024ULL * 1024ULL;
272
299
  static constexpr VkDeviceSize kStagingAlignment = 256;
300
+ // Per-batch (per beginFrame flush) allocation budget. With async upload
301
+ // submission, the ring head must never wrap past slices still being read by
302
+ // the GPU. Capping a single batch to kStagingSize / kMaxFramesInFlight
303
+ // guarantees that by the time the head wraps back, the slot that wrote
304
+ // those bytes has already had its uploadFences_ signalled (we wait on it
305
+ // in beginFrame before clearing uploadsInFlight_). When a single batch
306
+ // would exceed the budget the call site falls back to the synchronous
307
+ // per-tile upload path that already exists for the pre-init case.
308
+ static constexpr VkDeviceSize kStagingPerFrameBudget =
309
+ kStagingSize / kMaxFramesInFlight;
273
310
  VkBuffer stagingBuffer_ = VK_NULL_HANDLE;
274
311
  VkDeviceMemory stagingMemory_ = VK_NULL_HANDLE;
275
312
  uint8_t* stagingMapped_ = nullptr;
276
313
  VkDeviceSize stagingHead_ = 0;
314
+ // Bytes allocated from the staging ring since the last flush. Reset to 0
315
+ // inside flushPendingUploads() once those bytes have been moved into
316
+ // uploadsInFlight_[frameIndex_].
317
+ VkDeviceSize stagingBytesThisBatch_ = 0;
277
318
  VkFence uploadFences_[kMaxFramesInFlight] = {};
278
319
  std::vector<PendingUpload> pendingUploadsCurrent_; // queued, not yet submitted
279
320
  std::vector<PendingUpload> uploadsInFlight_[kMaxFramesInFlight]; // submitted