react-native-cesium 1.2.16 → 1.2.18

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.
@@ -80,7 +80,9 @@ android {
80
80
 
81
81
  buildTypes {
82
82
  debug {
83
- cppFlags "-O1 -g"
83
+ // -O2 (was -O1) so debug builds — the default dev/emulator workflow —
84
+ // aren't unfairly slow when profiling the renderer. -g keeps symbols.
85
+ cppFlags "-O2 -g"
84
86
  }
85
87
  release {
86
88
  cppFlags "-O2"
@@ -10,7 +10,9 @@
10
10
  // catastrophic cancellation in the shader.
11
11
  // - Single merged vertex + index buffer uploaded each frame.
12
12
  // - Reversed-Z infinite projection: depth clear = 0, compare = GREATER.
13
- // - Sky drawn first (no depth write), terrain drawn after.
13
+ // - Terrain drawn first (writes depth); sky drawn after with a depth test
14
+ // (no write) so the full-screen atmospheric shader only runs on pixels not
15
+ // covered by terrain.
14
16
  // - Water mask: UV computed from geographic lat/lon + tile bounds (not fragUV).
15
17
  // - Push constants: bytes 0-63 vertex (MVP), bytes 64-127 fragment.
16
18
  // - Triple-buffered persistent VkBuffers guarded by VkFence.
@@ -447,6 +449,29 @@ void VulkanBackend::pickPhysicalDevice() {
447
449
  supportsAnisotropy_ = feats.samplerAnisotropy == VK_TRUE;
448
450
  maxAnisotropy_ = props.limits.maxSamplerAnisotropy;
449
451
 
452
+ // One-time log of the selected GPU so we can tell a real hardware device from
453
+ // a software rasterizer (e.g. the Android Emulator's SwiftShader / a slow
454
+ // gfxstream translation layer), which is the dominant factor in emulator FPS.
455
+ const char* typeStr = "OTHER";
456
+ switch (props.deviceType) {
457
+ case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: typeStr = "DISCRETE_GPU"; break;
458
+ case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: typeStr = "INTEGRATED_GPU"; break;
459
+ case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: typeStr = "VIRTUAL_GPU"; break;
460
+ case VK_PHYSICAL_DEVICE_TYPE_CPU: typeStr = "CPU (software)"; break;
461
+ default: typeStr = "OTHER"; break;
462
+ }
463
+ LOGI("Vulkan GPU: \"%s\" type=%s apiVersion=%u.%u.%u driverVersion=0x%x",
464
+ props.deviceName, typeStr,
465
+ VK_VERSION_MAJOR(props.apiVersion),
466
+ VK_VERSION_MINOR(props.apiVersion),
467
+ VK_VERSION_PATCH(props.apiVersion),
468
+ props.driverVersion);
469
+ if (props.deviceType == VK_PHYSICAL_DEVICE_TYPE_CPU) {
470
+ LOGW("Selected a CPU/software Vulkan device — expect very low FPS. "
471
+ "This is typical on the Android Emulator without host-GPU acceleration; "
472
+ "measure on a physical device for representative performance.");
473
+ }
474
+
450
475
  const VkSampleCountFlags counts = props.limits.framebufferColorSampleCounts &
451
476
  props.limits.framebufferDepthSampleCounts;
452
477
  if (counts & VK_SAMPLE_COUNT_8_BIT) supportedMsaaMask_ = VK_SAMPLE_COUNT_8_BIT;
@@ -582,6 +607,32 @@ void VulkanBackend::createSwapchain() {
582
607
  VkSwapchainCreateInfoKHR swapInfo{};
583
608
  swapInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
584
609
  swapInfo.surface = surface_;
610
+ // ── Surface rotation (Android pre-rotation) ──────────────────────────────
611
+ // We deliberately let the Android Compositor handle device rotation instead
612
+ // of pre-rotating the scene ourselves: present with an IDENTITY transform so
613
+ // the presentation engine rotates the rendered image to the current device
614
+ // orientation before scan-out.
615
+ //
616
+ // The alternative — preTransform = caps.currentTransform — only renders
617
+ // correctly if the app ALSO swaps the swapchain extent for 90/270 rotations
618
+ // AND bakes a matching rotation into every MVP/VP matrix. Doing neither (as
619
+ // before) made the scene render sideways once the device left its identity
620
+ // orientation: in landscape Android reports ROTATE_90/270, the compositor
621
+ // was told "already handled", and the un-rotated terrain appeared rotated
622
+ // 90° (ground stuck on the side of the screen). Since the swapchain image is
623
+ // the engine's only render target and modern Android display processors
624
+ // rotate essentially for free, the compositor path is both correct and cheap.
625
+ //
626
+ // Consequence: while the device is rotated, preTransform != currentTransform,
627
+ // so vkAcquireNextImageKHR / vkQueuePresentKHR return VK_SUBOPTIMAL_KHR every
628
+ // frame. That is expected here and must NOT trigger swapchain recreation (see
629
+ // beginFrame() / endFrame()); genuine size/orientation changes are driven by
630
+ // surfaceChanged() -> resize() instead.
631
+ VkSurfaceTransformFlagBitsKHR preTransform = caps.currentTransform;
632
+ if (caps.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) {
633
+ preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
634
+ }
635
+
585
636
  swapInfo.minImageCount = imageCount;
586
637
  swapInfo.imageFormat = swapchainFormat_;
587
638
  swapInfo.imageColorSpace = colorSpace;
@@ -589,7 +640,7 @@ void VulkanBackend::createSwapchain() {
589
640
  swapInfo.imageArrayLayers = 1;
590
641
  swapInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
591
642
  swapInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
592
- swapInfo.preTransform = caps.currentTransform;
643
+ swapInfo.preTransform = preTransform;
593
644
  swapInfo.compositeAlpha = compositeAlpha;
594
645
  swapInfo.presentMode = pickPresentMode();
595
646
  swapInfo.clipped = VK_TRUE;
@@ -1065,6 +1116,7 @@ struct PipelineBuildSpec {
1065
1116
  const VkVertexInputAttributeDescription* attributes;
1066
1117
  bool depthTest;
1067
1118
  bool depthWrite;
1119
+ VkCompareOp depthCompare;
1068
1120
  VkCullModeFlags cull;
1069
1121
  VkSampleCountFlagBits samples;
1070
1122
  };
@@ -1115,7 +1167,7 @@ VkPipeline buildPipeline(VkDevice device,
1115
1167
  depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
1116
1168
  depthStencil.depthTestEnable = spec.depthTest ? VK_TRUE : VK_FALSE;
1117
1169
  depthStencil.depthWriteEnable = spec.depthWrite ? VK_TRUE : VK_FALSE;
1118
- depthStencil.depthCompareOp = VK_COMPARE_OP_GREATER; // Reversed-Z
1170
+ depthStencil.depthCompareOp = spec.depthCompare; // Reversed-Z (GREATER family)
1119
1171
 
1120
1172
  VkPipelineColorBlendAttachmentState colorBlendAttach{};
1121
1173
  colorBlendAttach.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
@@ -1166,8 +1218,15 @@ void VulkanBackend::createSkyPipeline() {
1166
1218
  spec.bindings = nullptr;
1167
1219
  spec.attributeCount = 0;
1168
1220
  spec.attributes = nullptr;
1169
- spec.depthTest = false;
1221
+ // Depth-test the sky against the terrain (which is drawn first and writes
1222
+ // depth) so the expensive atmospheric raymarch only runs on pixels that
1223
+ // aren't covered by geometry. sky.vert emits z=0 (the reversed-Z far plane)
1224
+ // and the depth buffer is cleared to 0, so GREATER_OR_EQUAL passes only where
1225
+ // no terrain wrote a closer (>0) depth. No depth write — the sky never
1226
+ // occludes anything.
1227
+ spec.depthTest = true;
1170
1228
  spec.depthWrite = false;
1229
+ spec.depthCompare = VK_COMPARE_OP_GREATER_OR_EQUAL;
1171
1230
  spec.cull = VK_CULL_MODE_NONE;
1172
1231
  spec.samples = sampleCount_;
1173
1232
  skyPipeline_ = buildPipeline(device_, skyPipelineLayout_, renderPass_,
@@ -1207,6 +1266,7 @@ void VulkanBackend::createTerrainPipeline() {
1207
1266
  spec.attributes = attrs;
1208
1267
  spec.depthTest = true;
1209
1268
  spec.depthWrite = true;
1269
+ spec.depthCompare = VK_COMPARE_OP_GREATER; // Reversed-Z
1210
1270
  spec.cull = VK_CULL_MODE_BACK_BIT;
1211
1271
  spec.samples = sampleCount_;
1212
1272
  return buildPipeline(device_, terrainPipelineLayout_, renderPass_,
@@ -2042,13 +2102,18 @@ void VulkanBackend::beginFrame(const FrameParams& /*params*/) {
2042
2102
  frameBegan_ = false;
2043
2103
  return;
2044
2104
  }
2045
- if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
2105
+ // Only OUT_OF_DATE forces a recreate. VK_SUBOPTIMAL_KHR is expected on every
2106
+ // frame while the device is rotated: we present with an IDENTITY preTransform
2107
+ // and let the compositor rotate (see createSwapchain). The acquired image is
2108
+ // still valid, so we proceed and use it. Genuine size/orientation changes
2109
+ // arrive via surfaceChanged() -> resize() -> needsSwapchainRecreate_.
2110
+ if (result == VK_ERROR_OUT_OF_DATE_KHR) {
2046
2111
  recreateSwapchain();
2047
2112
  result = vkAcquireNextImageKHR(device_, swapchain_, UINT64_MAX,
2048
2113
  imageAvailableSemaphores_[frameIndex_],
2049
2114
  VK_NULL_HANDLE, &imageIndex_);
2050
2115
  }
2051
- if (result != VK_SUCCESS) {
2116
+ if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
2052
2117
  frameBegan_ = false;
2053
2118
  return;
2054
2119
  }
@@ -2100,31 +2165,11 @@ void VulkanBackend::drawScene(const FrameResult& frame) {
2100
2165
 
2101
2166
  VkCommandBuffer cmd = commandBuffers_[frameIndex_];
2102
2167
 
2103
- // ── Sky pass ──────────────────────────────────────────────────────────────
2104
- if (skyPipeline_ && skyDescSet_) {
2105
- SkyUBO skyU{};
2106
- const float* iv = glm::value_ptr(frame.invVP);
2107
- for (int i = 0; i < 16; ++i) skyU.invVP[i] = iv[i];
2108
- skyU.cameraEcef[0] = frame.cameraEcef.x;
2109
- skyU.cameraEcef[1] = frame.cameraEcef.y;
2110
- skyU.cameraEcef[2] = frame.cameraEcef.z;
2111
- float cl = sqrtf(frame.cameraEcef.x * frame.cameraEcef.x +
2112
- frame.cameraEcef.y * frame.cameraEcef.y +
2113
- frame.cameraEcef.z * frame.cameraEcef.z);
2114
- if (cl > 0.0f) {
2115
- skyU.lightDir[0] = frame.cameraEcef.x / cl;
2116
- skyU.lightDir[1] = frame.cameraEcef.y / cl;
2117
- skyU.lightDir[2] = frame.cameraEcef.z / cl;
2118
- }
2119
-
2120
- memcpy(skyUboMapped_, &skyU, sizeof(SkyUBO));
2121
-
2122
- vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, skyPipeline_);
2123
- vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, skyPipelineLayout_,
2124
- 0, 1, &skyDescSet_, 0, nullptr);
2125
- vkCmdDraw(cmd, 3, 1, 0, 0);
2126
- }
2127
-
2168
+ // ── Terrain pass (opaque, writes depth) ─────────────────────────────────────
2169
+ // Drawn FIRST so the sky pass below can depth-test against the terrain and
2170
+ // skip the expensive full-screen atmospheric raymarch on every covered pixel.
2171
+ // Wrapped in a lambda so its early-outs still fall through to the sky pass.
2172
+ auto drawTerrain = [&]() {
2128
2173
  if (frame.draws.empty() || !terrainSolidPipeline_ || !terrainDitherPipeline_) {
2129
2174
  return;
2130
2175
  }
@@ -2277,6 +2322,36 @@ void VulkanBackend::drawScene(const FrameResult& frame) {
2277
2322
  uint32_t firstIndex = draw.indexByteOffset / sizeof(uint32_t);
2278
2323
  vkCmdDrawIndexed(cmd, draw.indexCount, 1, firstIndex, 0, 0);
2279
2324
  }
2325
+ }; // drawTerrain
2326
+ drawTerrain();
2327
+
2328
+ // ── Sky pass ────────────────────────────────────────────────────────────────
2329
+ // Drawn LAST and depth-tested (no write): the raymarch only executes on pixels
2330
+ // where no terrain was rendered (depth still at the cleared reversed-Z far
2331
+ // plane). This avoids shading the full screen and then overdrawing it.
2332
+ if (skyPipeline_ && skyDescSet_) {
2333
+ SkyUBO skyU{};
2334
+ const float* iv = glm::value_ptr(frame.invVP);
2335
+ for (int i = 0; i < 16; ++i) skyU.invVP[i] = iv[i];
2336
+ skyU.cameraEcef[0] = frame.cameraEcef.x;
2337
+ skyU.cameraEcef[1] = frame.cameraEcef.y;
2338
+ skyU.cameraEcef[2] = frame.cameraEcef.z;
2339
+ float cl = sqrtf(frame.cameraEcef.x * frame.cameraEcef.x +
2340
+ frame.cameraEcef.y * frame.cameraEcef.y +
2341
+ frame.cameraEcef.z * frame.cameraEcef.z);
2342
+ if (cl > 0.0f) {
2343
+ skyU.lightDir[0] = frame.cameraEcef.x / cl;
2344
+ skyU.lightDir[1] = frame.cameraEcef.y / cl;
2345
+ skyU.lightDir[2] = frame.cameraEcef.z / cl;
2346
+ }
2347
+
2348
+ memcpy(skyUboMapped_, &skyU, sizeof(SkyUBO));
2349
+
2350
+ vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, skyPipeline_);
2351
+ vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, skyPipelineLayout_,
2352
+ 0, 1, &skyDescSet_, 0, nullptr);
2353
+ vkCmdDraw(cmd, 3, 1, 0, 0);
2354
+ }
2280
2355
  }
2281
2356
 
2282
2357
  void VulkanBackend::endFrame() {
@@ -2311,7 +2386,12 @@ void VulkanBackend::endFrame() {
2311
2386
  presentInfo.pImageIndices = &imageIndex_;
2312
2387
 
2313
2388
  VkResult result = vkQueuePresentKHR(presentQueue_, &presentInfo);
2314
- if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
2389
+ // VK_SUBOPTIMAL_KHR is expected every frame while the device is rotated (we
2390
+ // present an IDENTITY preTransform and let the compositor rotate — see
2391
+ // createSwapchain), so it must NOT trigger a recreate or we'd thrash the
2392
+ // swapchain every frame. Real changes come through surfaceChanged() ->
2393
+ // resize() -> needsSwapchainRecreate_.
2394
+ if (result == VK_ERROR_OUT_OF_DATE_KHR) {
2315
2395
  needsSwapchainRecreate_ = true;
2316
2396
  }
2317
2397
 
@@ -38,11 +38,11 @@ void main() {
38
38
  float tmin = max(at.x, 0.0);
39
39
  if (tmin >= tmax) { outColor = vec4(0, 0, 0, 1); return; }
40
40
 
41
- // Sample counts halved from 16/4 to 8/2 — visually negligible at typical
41
+ // Sample counts reduced 16/4 -> 8/2 -> 6/2 — visually negligible at typical
42
42
  // phone DPI / viewport sizes (a 24-bit screen can't resolve the difference
43
- // in atmospheric scattering past ~6 primary samples), saves ~half the
44
- // expensive `exp` calls per pixel.
45
- const int S = 8, L = 2;
43
+ // in atmospheric scattering past ~6 primary samples), and cuts the expensive
44
+ // per-pixel `exp` calls further (helps weak/software GPUs most).
45
+ const int S = 6, L = 2;
46
46
  float ss = (tmax - tmin) / float(S);
47
47
  vec3 rayl = vec3(0), miel = vec3(0);
48
48
  float oR = 0.0, oM = 0.0;
@@ -16,7 +16,9 @@
16
16
  // ellipsoidHeightMeters(wp) in the old shader.
17
17
  // • Single merged vertex + index buffer uploaded each frame.
18
18
  // • Reversed-Z infinite projection: depth clear = 0, compare = GREATER.
19
- // • Sky drawn first (no depth write), terrain drawn after.
19
+ // • Terrain drawn first (writes depth); sky drawn after with a depth test
20
+ // (no write) so the full-screen atmospheric shader only runs on pixels not
21
+ // covered by terrain.
20
22
  // • Triple-buffered persistent MTLBuffers: one slot per in-flight frame.
21
23
 
22
24
  #import <Foundation/Foundation.h>
@@ -335,9 +337,10 @@ fragment float4 skyFragment(SkyVOut in [[stage_in]],
335
337
  // underground sample heights to zero (ground-level density).
336
338
  float tmax=at.y;
337
339
  float tmin=max(at.x,0.f);if(tmin>=tmax)return float4(0,0,0,1);
338
- // Halved from 16/4 to 8/2 — visually negligible at typical phone DPI
339
- // (atmospheric scattering banding is sub-pixel at this sample count).
340
- const int S=8,L=2;
340
+ // Reduced 16/4 -> 8/2 -> 6/2 — visually negligible at typical phone DPI
341
+ // (atmospheric scattering banding is sub-pixel at this sample count) and
342
+ // cuts per-pixel `exp` calls further. Kept in sync with the Vulkan sky.frag.
343
+ const int S=6,L=2;
341
344
  float ss=(tmax-tmin)/float(S);
342
345
  float3 rayl=0,miel=0;float oR=0,oM=0;
343
346
  for(int i=0;i<S;++i){
@@ -542,7 +545,11 @@ void MetalBackend::buildRenderPipelines() {
542
545
  }
543
546
  if (!skyDepthState_) {
544
547
  MTLDepthStencilDescriptor* sd = [MTLDepthStencilDescriptor new];
545
- sd.depthCompareFunction = MTLCompareFunctionAlways;
548
+ // Sky is drawn after terrain with a depth test (no write). skyVertex emits
549
+ // z=0 (the reversed-Z far plane) and the depth buffer clears to 0, so
550
+ // GreaterEqual passes only where no terrain wrote a closer (>0) depth —
551
+ // i.e. the raymarch only runs on uncovered pixels.
552
+ sd.depthCompareFunction = MTLCompareFunctionGreaterEqual;
546
553
  sd.depthWriteEnabled = NO;
547
554
  skyDepthState_ = (__bridge_retained void*)
548
555
  [dev newDepthStencilStateWithDescriptor:sd];
@@ -550,6 +557,8 @@ void MetalBackend::buildRenderPipelines() {
550
557
  }
551
558
 
552
559
  void MetalBackend::setMsaaSampleCount(int sc) {
560
+ if (!device_) return;
561
+
553
562
  int n = sc;
554
563
  if (n != 2 && n != 4) n = 1;
555
564
  id<MTLDevice> dev = (__bridge id<MTLDevice>)device_;
@@ -594,24 +603,26 @@ void MetalBackend::resize(int width, int height) {
594
603
  }
595
604
 
596
605
  void MetalBackend::shutdown() {
597
- // Drain in-flight frames: wait until all GPU completion handlers have fired
598
- // before releasing the semaphore and the persistent buffers they reference.
599
- // Bounded wait — if a slot doesn't release in 1 s we are almost certainly
600
- // looking at a hung GPU command. Forcing release lets the app exit/teardown
601
- // cleanly rather than ANRing on backgrounding.
606
+ // Idempotent — CesiumBridge calls shutdown() explicitly and the unique_ptr
607
+ // dtor calls it again; during Fast Refresh teardown may also overlap with
608
+ // in-flight Metal completion handlers that captured the frame semaphore.
609
+ if (!device_) return;
610
+
611
+ // Drain in-flight frame slots: each beginFrame wait consumes a token and
612
+ // endFrame's GPU completion handler signals it back. Blocking here until
613
+ // every committed command buffer has finished ensures no handler signals
614
+ // the semaphore after we CFRelease it (Fast Refresh / unmount crash).
602
615
  if (frameSemaphore_) {
603
616
  dispatch_semaphore_t sem = (__bridge dispatch_semaphore_t)frameSemaphore_;
604
617
  for (int i = 0; i < kMaxFramesInFlight; ++i) {
605
- dispatch_time_t deadline = dispatch_time(DISPATCH_TIME_NOW,
606
- (int64_t)NSEC_PER_SEC);
607
- if (dispatch_semaphore_wait(sem, deadline) != 0) {
608
- NSLog(@"[CesiumMetal] shutdown semaphore wait timed out at slot %d; "
609
- @"force-releasing and continuing teardown", i);
610
- break;
611
- }
618
+ dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
619
+ }
620
+ for (int i = 0; i < kMaxFramesInFlight; ++i) {
621
+ dispatch_semaphore_signal(sem);
612
622
  }
613
- CFRelease(frameSemaphore_);
623
+ void* semToRelease = frameSemaphore_;
614
624
  frameSemaphore_ = nullptr;
625
+ CFRelease(semToRelease);
615
626
  }
616
627
 
617
628
  for (int i = 0; i < kMaxFramesInFlight; ++i) {
@@ -733,6 +744,8 @@ void* MetalBackend::createRasterTexture(const uint8_t* pixels,
733
744
  // ── Per-frame ─────────────────────────────────────────────────────────────────
734
745
 
735
746
  void MetalBackend::beginFrame(const FrameParams& /*params*/) {
747
+ if (!device_ || !frameSemaphore_) return;
748
+
736
749
  // Acquire a frame slot. Blocks if kMaxFramesInFlight frames are already
737
750
  // queued on the GPU. The completion handler in endFrame() signals the
738
751
  // semaphore to release the slot.
@@ -797,32 +810,11 @@ void MetalBackend::drawScene(const FrameResult& frame) {
797
810
  id<MTLRenderCommandEncoder> enc = (__bridge id<MTLRenderCommandEncoder>)currentEncoder_;
798
811
  id<MTLDevice> dev = (__bridge id<MTLDevice>)device_;
799
812
 
800
- // ── Sky pass ──────────────────────────────────────────────────────────────
801
- if (skyPipeline_ && skyDepthState_) {
802
- SkyUniforms skyU{};
803
- const float* iv = glm::value_ptr(frame.invVP);
804
- for (int i = 0; i < 16; ++i) skyU.invVP[i] = iv[i];
805
- skyU.cameraEcef[0] = frame.cameraEcef.x;
806
- skyU.cameraEcef[1] = frame.cameraEcef.y;
807
- skyU.cameraEcef[2] = frame.cameraEcef.z;
808
- float cl = sqrtf(frame.cameraEcef.x * frame.cameraEcef.x +
809
- frame.cameraEcef.y * frame.cameraEcef.y +
810
- frame.cameraEcef.z * frame.cameraEcef.z);
811
- if (cl > 0.0f) {
812
- skyU.lightDir[0] = frame.cameraEcef.x / cl;
813
- skyU.lightDir[1] = frame.cameraEcef.y / cl;
814
- skyU.lightDir[2] = frame.cameraEcef.z / cl;
815
- }
816
-
817
- [enc setRenderPipelineState:(__bridge id<MTLRenderPipelineState>)skyPipeline_];
818
- [enc setDepthStencilState:(__bridge id<MTLDepthStencilState>)skyDepthState_];
819
- [enc setCullMode:MTLCullModeNone];
820
- [enc setVertexBytes:&skyU length:sizeof(skyU) atIndex:0];
821
- [enc setFragmentBytes:&skyU length:sizeof(skyU) atIndex:0];
822
- [enc drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
823
- }
824
-
825
813
  // ── Terrain pass ──────────────────────────────────────────────────────────
814
+ // Drawn FIRST (writes depth) so the sky pass below can depth-test against the
815
+ // terrain and skip the expensive full-screen atmospheric raymarch on every
816
+ // covered pixel. Wrapped in a lambda so its early-outs fall through to sky.
817
+ auto drawTerrain = [&]() {
826
818
  if (frame.draws.empty() ||
827
819
  !terrainSolidPipeline_ || !terrainDitherPipeline_ || !terrainDepthState_) {
828
820
  return;
@@ -1013,6 +1005,35 @@ void MetalBackend::drawScene(const FrameResult& frame) {
1013
1005
  indexBuffer:idxBuf
1014
1006
  indexBufferOffset:draw.indexByteOffset];
1015
1007
  }
1008
+ }; // drawTerrain
1009
+ drawTerrain();
1010
+
1011
+ // ── Sky pass ──────────────────────────────────────────────────────────────
1012
+ // Drawn LAST, depth-tested (no write): only shades pixels not covered by
1013
+ // terrain (depth still at the cleared reversed-Z far plane).
1014
+ if (skyPipeline_ && skyDepthState_) {
1015
+ SkyUniforms skyU{};
1016
+ const float* iv = glm::value_ptr(frame.invVP);
1017
+ for (int i = 0; i < 16; ++i) skyU.invVP[i] = iv[i];
1018
+ skyU.cameraEcef[0] = frame.cameraEcef.x;
1019
+ skyU.cameraEcef[1] = frame.cameraEcef.y;
1020
+ skyU.cameraEcef[2] = frame.cameraEcef.z;
1021
+ float cl = sqrtf(frame.cameraEcef.x * frame.cameraEcef.x +
1022
+ frame.cameraEcef.y * frame.cameraEcef.y +
1023
+ frame.cameraEcef.z * frame.cameraEcef.z);
1024
+ if (cl > 0.0f) {
1025
+ skyU.lightDir[0] = frame.cameraEcef.x / cl;
1026
+ skyU.lightDir[1] = frame.cameraEcef.y / cl;
1027
+ skyU.lightDir[2] = frame.cameraEcef.z / cl;
1028
+ }
1029
+
1030
+ [enc setRenderPipelineState:(__bridge id<MTLRenderPipelineState>)skyPipeline_];
1031
+ [enc setDepthStencilState:(__bridge id<MTLDepthStencilState>)skyDepthState_];
1032
+ [enc setCullMode:MTLCullModeNone];
1033
+ [enc setVertexBytes:&skyU length:sizeof(skyU) atIndex:0];
1034
+ [enc setFragmentBytes:&skyU length:sizeof(skyU) atIndex:0];
1035
+ [enc drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
1036
+ }
1016
1037
  }
1017
1038
 
1018
1039
  void MetalBackend::endFrame() {
@@ -1031,7 +1052,9 @@ void MetalBackend::endFrame() {
1031
1052
  // Signal the frame semaphore when the GPU is done with this frame's buffers.
1032
1053
  void* semPtr = frameSemaphore_;
1033
1054
  [cb addCompletedHandler:^(id<MTLCommandBuffer> __unused) {
1034
- dispatch_semaphore_signal((__bridge dispatch_semaphore_t)semPtr);
1055
+ if (semPtr) {
1056
+ dispatch_semaphore_signal((__bridge dispatch_semaphore_t)semPtr);
1057
+ }
1035
1058
  }];
1036
1059
 
1037
1060
  [cb presentDrawable:drawable];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-cesium",
3
- "version": "1.2.16",
3
+ "version": "1.2.18",
4
4
  "description": "Experimental Cesium rendering for React Native via Nitro",
5
5
  "main": "./lib/commonjs/index.js",
6
6
  "module": "./lib/module/index.js",
@@ -76,13 +76,13 @@
76
76
  "@semantic-release/changelog": "^6.0.3",
77
77
  "@semantic-release/git": "^10.0.1",
78
78
  "@types/jest": "^29.5.14",
79
- "@types/react": "19.2.14",
79
+ "@types/react": "19.2.17",
80
80
  "conventional-changelog-conventionalcommits": "^9.3.1",
81
81
  "nitrogen": "0.35.9",
82
82
  "react": "19.2.3",
83
- "react-native": "0.85.3",
84
- "react-native-builder-bob": "^0.41.0",
85
- "semantic-release": "^25.0.3",
83
+ "react-native": "0.86.0",
84
+ "react-native-builder-bob": "^0.43.0",
85
+ "semantic-release": "^25.0.5",
86
86
  "typescript": "^5.9.3"
87
87
  },
88
88
  "peerDependencies": {