react-native-cesium 1.0.1

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 (129) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +12 -0
  3. package/README.md +308 -0
  4. package/ReactNativeCesium.podspec +54 -0
  5. package/android/CMakeLists.txt +146 -0
  6. package/android/build.gradle +178 -0
  7. package/android/fix-prefab.gradle +51 -0
  8. package/android/gradle.properties +5 -0
  9. package/android/src/main/AndroidManifest.xml +5 -0
  10. package/android/src/main/assets/cacert.pem +3529 -0
  11. package/android/src/main/cpp/CesiumBridgeAndroid.cpp +487 -0
  12. package/android/src/main/cpp/CesiumBridgeAndroid.h +94 -0
  13. package/android/src/main/cpp/cpp-adapter.cpp +17 -0
  14. package/android/src/main/cpp/vulkan/VulkanBackend.cpp +1781 -0
  15. package/android/src/main/cpp/vulkan/VulkanBackend.h +190 -0
  16. package/android/src/main/cpp/vulkan/shaders/sky.frag +86 -0
  17. package/android/src/main/cpp/vulkan/shaders/sky.vert +9 -0
  18. package/android/src/main/cpp/vulkan/shaders/terrain.frag +144 -0
  19. package/android/src/main/cpp/vulkan/shaders/terrain.vert +25 -0
  20. package/android/src/main/java/com/margelo/nitro/reactnativecesium/CesiumBridgeJNI.kt +84 -0
  21. package/android/src/main/java/com/margelo/nitro/reactnativecesium/HybridCesiumView.kt +225 -0
  22. package/android/src/main/java/com/reactnativecesium/ReactNativeCesiumPackage.kt +28 -0
  23. package/cpp/engine/CesiumEngine.cpp +413 -0
  24. package/cpp/engine/CesiumEngine.hpp +90 -0
  25. package/cpp/engine/DeletionQueue.cpp +28 -0
  26. package/cpp/engine/DeletionQueue.hpp +28 -0
  27. package/cpp/engine/GlobeCamera.cpp +181 -0
  28. package/cpp/engine/GlobeCamera.hpp +64 -0
  29. package/cpp/engine/GltfToMesh.cpp +154 -0
  30. package/cpp/engine/GltfToMesh.hpp +20 -0
  31. package/cpp/engine/ResourcePreparer.cpp +224 -0
  32. package/cpp/engine/ResourcePreparer.hpp +96 -0
  33. package/cpp/engine/TaskProcessor.cpp +61 -0
  34. package/cpp/engine/TaskProcessor.hpp +43 -0
  35. package/cpp/engine/TileLifecycleManager.cpp +30 -0
  36. package/cpp/engine/TileLifecycleManager.hpp +25 -0
  37. package/cpp/fmt/format.h +9 -0
  38. package/cpp/renderer/IGPUBackend.hpp +22 -0
  39. package/cpp/renderer/RenderTypes.hpp +112 -0
  40. package/cpp/renderer/ShaderTypes.hpp +29 -0
  41. package/example/README.md +43 -0
  42. package/example/react-native-cesium-example.jpg +0 -0
  43. package/ios/CesiumBridge.h +55 -0
  44. package/ios/CesiumBridge.mm +393 -0
  45. package/ios/HybridCesiumView.swift +299 -0
  46. package/ios/ReactNativeCesium-umbrella.h +16 -0
  47. package/ios/metal/MetalBackend.h +82 -0
  48. package/ios/metal/MetalBackend.mm +839 -0
  49. package/ios/react_native_cesium_ensure_native.rb +12 -0
  50. package/ios/react_native_cesium_post_install.rb +30 -0
  51. package/lib/commonjs/CesiumView.js +13 -0
  52. package/lib/commonjs/CesiumView.js.map +1 -0
  53. package/lib/commonjs/index.js +13 -0
  54. package/lib/commonjs/index.js.map +1 -0
  55. package/lib/commonjs/package.json +1 -0
  56. package/lib/commonjs/specs/cesium-view.nitro.js +6 -0
  57. package/lib/commonjs/specs/cesium-view.nitro.js.map +1 -0
  58. package/lib/module/CesiumView.js +9 -0
  59. package/lib/module/CesiumView.js.map +1 -0
  60. package/lib/module/index.js +4 -0
  61. package/lib/module/index.js.map +1 -0
  62. package/lib/module/specs/cesium-view.nitro.js +4 -0
  63. package/lib/module/specs/cesium-view.nitro.js.map +1 -0
  64. package/lib/typescript/src/CesiumView.d.ts +5 -0
  65. package/lib/typescript/src/CesiumView.d.ts.map +1 -0
  66. package/lib/typescript/src/index.d.ts +3 -0
  67. package/lib/typescript/src/index.d.ts.map +1 -0
  68. package/lib/typescript/src/specs/cesium-view.nitro.d.ts +43 -0
  69. package/lib/typescript/src/specs/cesium-view.nitro.d.ts.map +1 -0
  70. package/nitro.json +30 -0
  71. package/nitrogen/generated/.gitattributes +1 -0
  72. package/nitrogen/generated/android/ReactNativeCesium+autolinking.cmake +83 -0
  73. package/nitrogen/generated/android/ReactNativeCesium+autolinking.gradle +27 -0
  74. package/nitrogen/generated/android/ReactNativeCesiumOnLoad.cpp +58 -0
  75. package/nitrogen/generated/android/ReactNativeCesiumOnLoad.hpp +34 -0
  76. package/nitrogen/generated/android/c++/JCameraState.hpp +81 -0
  77. package/nitrogen/generated/android/c++/JCesiumMetrics.hpp +81 -0
  78. package/nitrogen/generated/android/c++/JFunc_void_CesiumMetrics.hpp +78 -0
  79. package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.cpp +178 -0
  80. package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.hpp +83 -0
  81. package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.cpp +92 -0
  82. package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.hpp +49 -0
  83. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/CameraState.kt +56 -0
  84. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/CesiumMetrics.kt +56 -0
  85. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/Func_void_CesiumMetrics.kt +80 -0
  86. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/HybridCesiumViewSpec.kt +126 -0
  87. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/ReactNativeCesiumOnLoad.kt +35 -0
  88. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/views/HybridCesiumViewManager.kt +80 -0
  89. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/views/HybridCesiumViewStateUpdater.kt +23 -0
  90. package/nitrogen/generated/ios/ReactNativeCesium+autolinking.rb +60 -0
  91. package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Bridge.cpp +57 -0
  92. package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Bridge.hpp +164 -0
  93. package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Umbrella.hpp +53 -0
  94. package/nitrogen/generated/ios/ReactNativeCesiumAutolinking.mm +33 -0
  95. package/nitrogen/generated/ios/ReactNativeCesiumAutolinking.swift +26 -0
  96. package/nitrogen/generated/ios/c++/HybridCesiumViewSpecSwift.cpp +11 -0
  97. package/nitrogen/generated/ios/c++/HybridCesiumViewSpecSwift.hpp +157 -0
  98. package/nitrogen/generated/ios/c++/views/HybridCesiumViewComponent.mm +167 -0
  99. package/nitrogen/generated/ios/swift/CameraState.swift +59 -0
  100. package/nitrogen/generated/ios/swift/CesiumMetrics.swift +59 -0
  101. package/nitrogen/generated/ios/swift/Func_void_CameraState.swift +46 -0
  102. package/nitrogen/generated/ios/swift/Func_void_CesiumMetrics.swift +46 -0
  103. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  104. package/nitrogen/generated/ios/swift/HybridCesiumViewSpec.swift +65 -0
  105. package/nitrogen/generated/ios/swift/HybridCesiumViewSpec_cxx.swift +306 -0
  106. package/nitrogen/generated/shared/c++/CameraState.hpp +107 -0
  107. package/nitrogen/generated/shared/c++/CesiumMetrics.hpp +107 -0
  108. package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.cpp +42 -0
  109. package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.hpp +90 -0
  110. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.cpp +182 -0
  111. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.hpp +122 -0
  112. package/nitrogen/generated/shared/json/CesiumViewConfig.json +19 -0
  113. package/package.json +140 -0
  114. package/react-native.config.js +16 -0
  115. package/scripts/cesium/build.mjs +901 -0
  116. package/scripts/cesium/clean.mjs +35 -0
  117. package/scripts/cesium/clone.mjs +32 -0
  118. package/scripts/cesium/ensure-native.mjs +116 -0
  119. package/scripts/cesium/vcpkg-ports/zstd/fix-emscripten-and-clang-cl.patch +31 -0
  120. package/scripts/cesium/vcpkg-ports/zstd/fix-windows-rc-compile.patch +13 -0
  121. package/scripts/cesium/vcpkg-ports/zstd/no-static-suffix.patch +13 -0
  122. package/scripts/cesium/vcpkg-ports/zstd/portfile.cmake +77 -0
  123. package/scripts/cesium/vcpkg-ports/zstd/usage +4 -0
  124. package/scripts/cesium/vcpkg-ports/zstd/vcpkg.json +23 -0
  125. package/scripts/cesium/vcpkg-triplets/arm64-ios-simulator.cmake +10 -0
  126. package/scripts/cesium/vcpkg-triplets/arm64-ios.cmake +14 -0
  127. package/src/CesiumView.tsx +13 -0
  128. package/src/index.ts +7 -0
  129. package/src/specs/cesium-view.nitro.ts +51 -0
@@ -0,0 +1,839 @@
1
+ // MetalBackend.mm — iOS Metal rendering backend
2
+ //
3
+ // Architecture:
4
+ // • RTC (Relative-to-Centre) tile-local float positions on the GPU.
5
+ // Vertex positions are stored relative to each tile's RTC centre so that
6
+ // the values remain small (tile radius, not full ECEF magnitude), giving
7
+ // full float32 precision for geometry at any altitude.
8
+ // • Per-tile MVP matrix computed on the CPU in double precision:
9
+ // MVP = P_double * R_double * T_double(rtcCentre − camera)
10
+ // then cast to float32 for the GPU. This avoids the large float32
11
+ // cancellation error that would arise from computing the camera offset
12
+ // on the GPU.
13
+ // • Vertex altitude attribute (metres, computed from double-precision Bowring
14
+ // formula on the CPU). Passed straight to the fragment shader — eliminates
15
+ // the ±50 m float32 catastrophic-cancellation error of
16
+ // ellipsoidHeightMeters(wp) in the old shader.
17
+ // • Single merged vertex + index buffer uploaded each frame.
18
+ // • Reversed-Z infinite projection: depth clear = 0, compare = GREATER.
19
+ // • Sky drawn first (no depth write), terrain drawn after.
20
+ // • Triple-buffered persistent MTLBuffers: one slot per in-flight frame.
21
+
22
+ #import <Foundation/Foundation.h>
23
+ #import <Metal/Metal.h>
24
+ #import <QuartzCore/CAMetalLayer.h>
25
+ #import <dispatch/dispatch.h>
26
+
27
+ #include "MetalBackend.h"
28
+
29
+ #include <glm/gtc/type_ptr.hpp>
30
+ #include <cstring> // memcpy
31
+ #include <algorithm> // std::max
32
+
33
+ // ── CPU-side uniform structs ──────────────────────────────────────────────────
34
+ // float3 in MSL is 16-byte aligned (same as float4), so we always use float[4]
35
+ // on the C++ side to keep sizes identical between CPU and GPU structs.
36
+
37
+ // Per-draw vertex uniforms: the tile's RTC MVP matrix (double-precision on CPU,
38
+ // cast to float32 for the GPU).
39
+ struct PerDrawVertexUniformsCPP {
40
+ float mvpMatrix[16]; // column-major float4x4 (64 bytes)
41
+ }; // total 64 bytes
42
+
43
+ struct SkyUniforms {
44
+ float invVP[16]; // column-major float4x4 (64 bytes)
45
+ float cameraEcef[4]; // xyz used, w=0 (16 bytes)
46
+ float lightDir[4]; // xyz used, w=0 (16 bytes)
47
+ }; // total 96 bytes
48
+
49
+ // Per-draw fragment uniforms: all overlay/water-mask parameters plus the RTC
50
+ // centre and camera-in-tile-space needed to reconstruct world position.
51
+ struct OverlayParamsCPP {
52
+ uint32_t hasOverlay; // offset 0
53
+ uint32_t isEllipsoidFallback; // offset 4
54
+ uint32_t isOnlyWater; // offset 8
55
+ uint32_t hasWaterMask; // offset 12
56
+ float translation[2]; // offset 16
57
+ float scale[2]; // offset 24
58
+ float wmWest; // offset 32
59
+ float wmSouth; // offset 36
60
+ float wmEast; // offset 40
61
+ float wmNorth; // offset 44
62
+ float wmTransX; // offset 48
63
+ float wmTransY; // offset 52
64
+ float wmScale; // offset 56
65
+ float _pad1; // offset 60
66
+ // RTC centre in absolute ECEF (float3). Fragment shader reconstructs world
67
+ // position as: wp = in.localPos + rtcCentre (see shader).
68
+ float rtcCenterX; // offset 64
69
+ float rtcCenterY; // offset 68
70
+ float rtcCenterZ; // offset 72
71
+ float _pad2; // offset 76
72
+ // Camera position in tile-local space (camera_ECEF − rtcCentre), cast to
73
+ // float32. Used for the view direction vector in lighting.
74
+ float cameraTilespaceX; // offset 80
75
+ float cameraTilespaceY; // offset 84
76
+ float cameraTilespaceZ; // offset 88
77
+ float _pad3; // offset 92
78
+ }; // total 96 bytes
79
+
80
+ // ── Shader source ─────────────────────────────────────────────────────────────
81
+
82
+ static NSString* const kTerrainShaderSrc = @R"MSL(
83
+ #include <metal_stdlib>
84
+ using namespace metal;
85
+
86
+ // ── Vertex shader ────────────────────────────────────────────────────────────
87
+ // RTC: vertex positions are stored relative to each tile's RTC centre (small
88
+ // floats). The per-tile MVP matrix was computed on the CPU entirely in double
89
+ // precision, so the large camera↔centre translation is represented accurately.
90
+ struct PerDrawVertexUniforms {
91
+ float4x4 mvpMatrix; // per-tile MVP: P_d * R_d * T_d(rtcCentre − camera) → float
92
+ };
93
+
94
+ struct TerrainVOut {
95
+ float4 position [[position]];
96
+ float3 localPos; // tile-local position (vertex − rtcCentre, float32)
97
+ float altitude; // ellipsoid height metres from vertex attribute (double source)
98
+ float2 uv;
99
+ };
100
+
101
+ vertex TerrainVOut terrainVertex(
102
+ uint vid [[vertex_id]],
103
+ const device packed_float3* pos [[buffer(0)]],
104
+ const device float* altitudes [[buffer(1)]],
105
+ constant PerDrawVertexUniforms& u [[buffer(2)]],
106
+ const device packed_float2* uvs [[buffer(3)]])
107
+ {
108
+ TerrainVOut o;
109
+ float3 lp = float3(pos[vid]);
110
+ o.position = u.mvpMatrix * float4(lp, 1.0f);
111
+ o.localPos = lp;
112
+ o.altitude = altitudes[vid];
113
+ o.uv = float2(uvs[vid]);
114
+ return o;
115
+ }
116
+
117
+ // ── Fragment shader ──────────────────────────────────────────────────────────
118
+ struct OverlayParams {
119
+ uint hasOverlay;
120
+ uint isEllipsoidFallback;
121
+ uint isOnlyWater;
122
+ uint hasWaterMask;
123
+ float translation[2];
124
+ float scale[2];
125
+ float wmWest, wmSouth, wmEast, wmNorth;
126
+ float wmTransX, wmTransY;
127
+ float wmScale;
128
+ float _pad1;
129
+ // RTC centre in ECEF (float). wp = in.localPos + rtcCentre gives a world
130
+ // position with ~0.75 m ULP (vs ~4 m for eyeRelPos + cameraEcef at altitude).
131
+ float rtcCenterX, rtcCenterY, rtcCenterZ;
132
+ float _pad2;
133
+ // Camera position in tile-local space: camera_ECEF − rtcCentre.
134
+ // Pre-computed on CPU in double, cast to float — used for lighting vd vector.
135
+ float cameraTilespaceX, cameraTilespaceY, cameraTilespaceZ;
136
+ float _pad3;
137
+ };
138
+
139
+ float3 hypsometricColor(float alt, float steep) {
140
+ float3 cDeep=float3(.03f,.20f,.46f),cShallow=float3(.08f,.44f,.78f);
141
+ float3 cLow=float3(.46f,.54f,.30f),cFoot=float3(.62f,.55f,.32f);
142
+ float3 cHigh=float3(.48f,.38f,.28f),cAlp=float3(.52f,.50f,.48f),cSnow=float3(.90f,.92f,.94f);
143
+ if(alt<0.f)return mix(cDeep,cShallow,smoothstep(-4500.f,-20.f,alt));
144
+ float3 c=cLow;
145
+ c=mix(c,cFoot,smoothstep(120.f,900.f,alt));
146
+ c=mix(c,cHigh,smoothstep(750.f,2000.f,alt));
147
+ c=mix(c,cAlp,smoothstep(1700.f,3600.f,alt));
148
+ c=mix(c,cSnow,smoothstep(3200.f,5200.f,alt));
149
+ float3 scree=float3(.38f,.35f,.32f);
150
+ float rock=saturate(steep*1.08f)*smoothstep(35.f,140.f,alt);
151
+ return mix(c,scree,rock);
152
+ }
153
+
154
+ fragment float4 terrainFragment(
155
+ TerrainVOut in [[stage_in]],
156
+ constant OverlayParams& ov [[buffer(0)]],
157
+ texture2d<float> overlayTex [[texture(0)]],
158
+ texture2d<float> waterMaskTex [[texture(1)]])
159
+ {
160
+ constexpr sampler s(filter::linear, address::clamp_to_edge);
161
+
162
+ // ── World position (RTC reconstruction) ──────────────────────────────────
163
+ // wp = tile-local position + RTC centre (both float32, sum at ~6.4 Mm).
164
+ // Precision: ~0.75 m ULP — much better than eyeRelPos + cameraEcef which
165
+ // had ~4 m ULP at typical camera altitudes. Used for lat/lon computation
166
+ // (water-mask UV) and lighting surface normal (up direction).
167
+ float3 rtcCentre = float3(ov.rtcCenterX, ov.rtcCenterY, ov.rtcCenterZ);
168
+ float3 wp = in.localPos + rtcCentre;
169
+ float pxy = sqrt(wp.x*wp.x + wp.y*wp.y);
170
+ float lon = atan2(wp.y, wp.x);
171
+
172
+ // Geodetic latitude (Bowring iteration in float32 — good to ~0.1 m for
173
+ // water-mask UV, which is all we need here).
174
+ const float a_e=6378137.f, b_e=6356752.31424518f;
175
+ const float ep2=(a_e*a_e-b_e*b_e)/(b_e*b_e), e2=1.f-(b_e*b_e)/(a_e*a_e);
176
+ float theta_g=atan2(wp.z*a_e, pxy*b_e);
177
+ float sg=sin(theta_g), cg=cos(theta_g);
178
+ float lat=atan2(wp.z+ep2*b_e*sg*sg*sg, pxy-e2*a_e*cg*cg*cg);
179
+
180
+ // ── Water-mask UV ─────────────────────────────────────────────────────────
181
+ // Cesium GEOGRAPHIC UV convention:
182
+ // U: 0=west … 1=east (unchanged for Metal texture)
183
+ // V: 0=south … 1=north (geographic, must be flipped for texture)
184
+ float wmU = (lon - ov.wmWest) / max(ov.wmEast - ov.wmWest, 1e-7f);
185
+ float v_geo = (lat - ov.wmSouth) / max(ov.wmNorth - ov.wmSouth, 1e-7f);
186
+ float2 geoUV = float2(wmU, v_geo) * ov.wmScale + float2(ov.wmTransX, ov.wmTransY);
187
+ float2 wmUV = clamp(float2(geoUV.x, 1.0f - geoUV.y), 0.0f, 1.0f);
188
+
189
+ float waterVal = (ov.isOnlyWater != 0u)
190
+ ? 1.0f
191
+ : waterMaskTex.sample(s, wmUV).r;
192
+
193
+ float3 lit;
194
+
195
+ if (ov.hasOverlay != 0u) {
196
+ float2 texUV = in.uv * float2(ov.scale[0], ov.scale[1])
197
+ + float2(ov.translation[0], ov.translation[1]);
198
+ texUV.y = 1.0f - texUV.y;
199
+ lit = overlayTex.sample(s, texUV).rgb;
200
+ } else {
201
+ // ── Lighting ──────────────────────────────────────────────────────────
202
+ // Normals from screen-space derivatives of tile-local position.
203
+ // (Using localPos rather than eyeRelPos makes no difference for the
204
+ // direction of the normal — only the scale changes.)
205
+ float3 dpdx = dfdx(in.localPos), dpdy = dfdy(in.localPos);
206
+ float3 rawN = cross(dpdx, dpdy);
207
+ float nLen = length(rawN);
208
+ float3 n = (nLen > 1e-8f) ? rawN / nLen : float3(0.f, 0.f, 1.f);
209
+
210
+ // View direction: camera−tile-centre minus vertex−tile-centre.
211
+ float3 cameraLocal = float3(ov.cameraTilespaceX, ov.cameraTilespaceY, ov.cameraTilespaceZ);
212
+ float3 vd = normalize(cameraLocal - in.localPos);
213
+ if (dot(n, vd) < 0.f) n = -n;
214
+
215
+ float3 gu = normalize(wp); // surface up (towards space)
216
+ float3 sun = normalize(gu + float3(0.3f, 0.2f, 0.1f));
217
+ float diff = saturate(dot(n, sun));
218
+ float amb = 0.18f;
219
+ float rim = saturate(1.f - dot(n, vd)) * 0.06f;
220
+ float steep = 1.f - saturate(abs(dot(n, gu)));
221
+
222
+ // ── Altitude (from vertex attribute — double-precision source) ────────
223
+ // In.altitude was computed on the CPU with Bowring formula in double
224
+ // (sub-mm accuracy). This completely avoids the ±50 m float32
225
+ // catastrophic cancellation of the old pxy/cos(lat) − N in the shader.
226
+ // The ellipsoid fallback is tessellated at −20 m → renders as ocean.
227
+ float rawAlt = in.altitude;
228
+
229
+ float3 oceanBlue = float3(0.04f, 0.26f, 0.52f);
230
+ // When a real water-mask texture is present it controls ocean pixels via
231
+ // waterVal, so clamp land altitude ≥ 0 to prevent residual float noise
232
+ // from ocean vertices colouring land blue. Without a water mask, use
233
+ // raw altitude so bathymetric depth renders as ocean.
234
+ float landAlt = (ov.hasWaterMask != 0u) ? max(rawAlt, 0.0f) : rawAlt;
235
+ float3 landLit = hypsometricColor(landAlt, steep) * (amb + diff * 0.72f + rim);
236
+ lit = mix(landLit, oceanBlue, waterVal);
237
+
238
+ // 1 km lat/lon grid — fades when sub-pixel to suppress Moiré.
239
+ float gridLat = 1000.f / 6371000.f;
240
+ float cosLat = max(cos(lat), 0.001f);
241
+ float gridLon = gridLat / cosLat;
242
+ float wLat = fwidth(lat) * 1.5f, wLon = fwidth(lon) * 1.5f;
243
+ float gridVis = clamp(min(gridLat / max(wLat, 1e-9f),
244
+ gridLon / max(wLon, 1e-9f)), 0.f, 1.f);
245
+ float latCell = lat - gridLat * floor(lat / gridLat);
246
+ float lonCell = lon - gridLon * floor(lon / gridLon);
247
+ float dLat = min(latCell, gridLat - latCell);
248
+ float dLon = min(lonCell, gridLon - lonCell);
249
+ float gridA = max(1.f - smoothstep(0.f, wLat, dLat),
250
+ 1.f - smoothstep(0.f, wLon, dLon)) * gridVis;
251
+ lit = mix(lit, float3(0.15f, 0.15f, 0.15f), gridA * 0.6f);
252
+ }
253
+
254
+ // Coastline outline from water mask (magenta for easy diagnostics).
255
+ if (ov.isEllipsoidFallback == 0u) {
256
+ float dWater = fwidth(waterVal);
257
+ float coastPx = abs(waterVal - 0.5f) / max(dWater * 0.5f, 0.01f);
258
+ float coastA = 1.f - smoothstep(0.f, 1.5f, coastPx);
259
+ lit = mix(lit, float3(0.18f, 0.14f, 0.10f), coastA * 0.7f);
260
+ }
261
+
262
+ return float4(lit, 1.f);
263
+ }
264
+ )MSL";
265
+
266
+ static NSString* const kSkyShaderSrc = @R"MSL(
267
+ #include <metal_stdlib>
268
+ using namespace metal;
269
+
270
+ struct SkyUniforms {
271
+ float4x4 invVP;
272
+ float4 cameraEcef; // xyz used, w=0
273
+ float4 lightDir; // xyz used, w=0
274
+ };
275
+ struct SkyVOut {
276
+ float4 position [[position]];
277
+ float2 ndc;
278
+ };
279
+
280
+ vertex SkyVOut skyVertex(uint vid [[vertex_id]],
281
+ constant SkyUniforms& u [[buffer(0)]])
282
+ {
283
+ float2 ps[3]={float2(-1,-1),float2(3,-1),float2(-1,3)};
284
+ SkyVOut o;
285
+ o.position=float4(ps[vid],0,1);
286
+ o.ndc=ps[vid];
287
+ return o;
288
+ }
289
+
290
+ constant float ER=6371000.f,AR=ER+111000.f;
291
+ constant float3 RAY=float3(3.2e-6f,8.5e-6f,38e-6f),MIE3=float3(1.8e-6f,1.8e-6f,1.8e-6f);
292
+ constant float MG=.76f,LINT=18.f;
293
+ float2 rsi(float3 o,float3 d,float r){
294
+ float a=dot(d,d),b=2*dot(d,o),c=dot(o,o)-r*r,disc=b*b-4*a*c;
295
+ if(disc<0)return float2(-1);
296
+ float sq=sqrt(disc);return float2((-b-sq)/(2*a),(-b+sq)/(2*a));
297
+ }
298
+ fragment float4 skyFragment(SkyVOut in [[stage_in]],
299
+ constant SkyUniforms& u [[buffer(0)]])
300
+ {
301
+ float4 wn=u.invVP*float4(in.ndc,-1,1),wf=u.invVP*float4(in.ndc,1,1);
302
+ float3 near3=wn.xyz/wn.w,far3=wf.xyz/wf.w;
303
+ float3 rd=normalize(far3-near3);
304
+ float3 org=u.cameraEcef.xyz;
305
+ float2 at=rsi(org,rd,AR);
306
+ if(at.y<0)return float4(0,0,0,1);
307
+ float2 er=rsi(org,rd,ER);
308
+ float tmax=at.y;if(er.x>0)tmax=min(tmax,er.x);
309
+ float tmin=max(at.x,0.f);if(tmin>=tmax)return float4(0,0,0,1);
310
+ const int S=16,L=4;
311
+ float ss=(tmax-tmin)/float(S);
312
+ float3 rayl=0,miel=0;float oR=0,oM=0;
313
+ for(int i=0;i<S;++i){
314
+ float t=tmin+(float(i)+.5f)*ss;
315
+ float3 sp=org+rd*t;float h=length(sp)-ER;
316
+ float hr=exp(-h/10000.f)*ss,hm=exp(-h/3200.f)*ss;
317
+ oR+=hr;oM+=hm;
318
+ float2 lh=rsi(sp,u.lightDir.xyz,AR);
319
+ if(lh.y>0){
320
+ float ls=lh.y/float(L);float lR=0,lM=0;bool bl=false;
321
+ for(int j=0;j<L;++j){
322
+ float3 lsp=sp+u.lightDir.xyz*((float(j)+.5f)*ls);
323
+ float lh2=length(lsp)-ER;if(lh2<0){bl=true;break;}
324
+ lR+=exp(-lh2/10000.f)*ls;lM+=exp(-lh2/3200.f)*ls;
325
+ }
326
+ if(!bl){float3 tau=RAY*(oR+lR)+MIE3*1.1f*(oM+lM);float3 att=exp(-tau);rayl+=hr*att;miel+=hm*att;}
327
+ }
328
+ }
329
+ float cosT=dot(rd,u.lightDir.xyz),cos2=cosT*cosT;
330
+ float rPh=.75f*(1+cos2);
331
+ float g2=MG*MG,mPh=1.5f*((1-g2)/(2+g2))*(1+cos2)/pow(1+g2-2*MG*cosT,1.5f);
332
+ float3 col=LINT*(rayl*RAY*rPh+miel*MIE3*mPh);
333
+ col=1-exp(-col*1.1f);col*=float3(.18f,.45f,1.f);
334
+ float3 nu=normalize(org);float up=saturate(dot(rd,nu));
335
+ float camH=max(0.f,length(org)-ER);
336
+ float hStr=saturate(1-camH/35000.f);
337
+ col=mix(col,float3(.40f,.62f,.96f),(1-smoothstep(.05f,.45f,up))*.40f*hStr);
338
+ return float4(col,1.f);
339
+ }
340
+ )MSL";
341
+
342
+ // ── Helper: ensure a persistent buffer is large enough, then memcpy into it ──
343
+ // Grows by doubling (rounded up to 4 KB) to amortise reallocation cost.
344
+ // Returns a non-owning bridge pointer to the buffer (ownership in *pBuf).
345
+ static id<MTLBuffer> ensureBuffer(void** pBuf, size_t* pCap,
346
+ size_t needed, const void* data,
347
+ id<MTLDevice> dev) {
348
+ if (needed == 0) return nil;
349
+ if (needed > *pCap) {
350
+ size_t newCap = std::max(needed, *pCap > 0 ? *pCap * 2 : needed);
351
+ newCap = (newCap + 4095UL) & ~4095UL; // round up to 4 KB page
352
+ if (*pBuf) CFRelease(*pBuf);
353
+ id<MTLBuffer> b = [dev newBufferWithLength:newCap
354
+ options:MTLResourceStorageModeShared];
355
+ *pBuf = (__bridge_retained void*)b;
356
+ *pCap = newCap;
357
+ }
358
+ id<MTLBuffer> b = (__bridge id<MTLBuffer>)*pBuf;
359
+ memcpy(b.contents, data, needed);
360
+ return b;
361
+ }
362
+
363
+ namespace reactnativecesium {
364
+
365
+ MetalBackend::MetalBackend()
366
+ : device_(nullptr), commandQueue_(nullptr), metalLayer_(nullptr),
367
+ terrainPipeline_(nullptr), skyPipeline_(nullptr),
368
+ terrainDepthState_(nullptr), skyDepthState_(nullptr),
369
+ depthTexture_(nullptr), fallbackTexture_(nullptr),
370
+ fallbackWaterMaskTexture_(nullptr),
371
+ currentDrawable_(nullptr), currentCommandBuffer_(nullptr),
372
+ currentEncoder_(nullptr),
373
+ frameSemaphore_(nullptr), frameIndex_(0) {
374
+ for (int i = 0; i < kMaxFramesInFlight; ++i) {
375
+ vtxBufs_[i] = idxBufs_[i] = uvBufs_[i] = altBufs_[i] = nullptr;
376
+ vtxCaps_[i] = idxCaps_[i] = uvCaps_[i] = altCaps_[i] = 0;
377
+ }
378
+ }
379
+
380
+ MetalBackend::~MetalBackend() { shutdown(); }
381
+
382
+ void MetalBackend::initialize(void* nativeSurface, int width, int height) {
383
+ viewportWidth_ = width;
384
+ viewportHeight_ = height;
385
+
386
+ id<MTLDevice> dev = MTLCreateSystemDefaultDevice();
387
+ device_ = (__bridge_retained void*)dev;
388
+ commandQueue_ = (__bridge_retained void*)[dev newCommandQueue];
389
+ metalLayer_ = nativeSurface; // weak — owned by CAMetalLayer
390
+
391
+ // Semaphore starts at kMaxFramesInFlight; each beginFrame decrements it and
392
+ // each GPU completion handler increments it.
393
+ dispatch_semaphore_t sem = dispatch_semaphore_create(kMaxFramesInFlight);
394
+ frameSemaphore_ = (__bridge_retained void*)sem;
395
+
396
+ CAMetalLayer* layer = (__bridge CAMetalLayer*)metalLayer_;
397
+ layer.device = dev;
398
+ layer.framebufferOnly = YES;
399
+
400
+ buildRenderPipelines();
401
+ createDepthTexture();
402
+ createFallbackTexture();
403
+ createWaterMaskFallback();
404
+ }
405
+
406
+ void MetalBackend::buildRenderPipelines() {
407
+ if (!device_) return;
408
+ id<MTLDevice> dev = (__bridge id<MTLDevice>)device_;
409
+ CAMetalLayer* layer = (__bridge CAMetalLayer*)metalLayer_;
410
+ const MTLPixelFormat colorFmt = layer.pixelFormat;
411
+
412
+ NSError* err = nil;
413
+
414
+ if (terrainPipeline_) {
415
+ CFRelease(terrainPipeline_);
416
+ terrainPipeline_ = nullptr;
417
+ }
418
+ if (skyPipeline_) {
419
+ CFRelease(skyPipeline_);
420
+ skyPipeline_ = nullptr;
421
+ }
422
+
423
+ id<MTLLibrary> tLib = [dev newLibraryWithSource:kTerrainShaderSrc
424
+ options:nil error:&err];
425
+ if (err) NSLog(@"[MetalBackend] terrain shader error: %@", err);
426
+
427
+ MTLRenderPipelineDescriptor* tDesc = [MTLRenderPipelineDescriptor new];
428
+ tDesc.vertexFunction = [tLib newFunctionWithName:@"terrainVertex"];
429
+ tDesc.fragmentFunction = [tLib newFunctionWithName:@"terrainFragment"];
430
+ tDesc.colorAttachments[0].pixelFormat = colorFmt;
431
+ tDesc.depthAttachmentPixelFormat = MTLPixelFormatDepth32Float;
432
+ tDesc.rasterSampleCount = (NSUInteger)std::max(1, sampleCount_);
433
+ terrainPipeline_ = (__bridge_retained void*)
434
+ [dev newRenderPipelineStateWithDescriptor:tDesc error:&err];
435
+ if (err) NSLog(@"[MetalBackend] terrain pipeline error: %@", err);
436
+
437
+ id<MTLLibrary> sLib = [dev newLibraryWithSource:kSkyShaderSrc
438
+ options:nil error:&err];
439
+ if (err) NSLog(@"[MetalBackend] sky shader error: %@", err);
440
+
441
+ MTLRenderPipelineDescriptor* sDesc = [MTLRenderPipelineDescriptor new];
442
+ sDesc.vertexFunction = [sLib newFunctionWithName:@"skyVertex"];
443
+ sDesc.fragmentFunction = [sLib newFunctionWithName:@"skyFragment"];
444
+ sDesc.colorAttachments[0].pixelFormat = colorFmt;
445
+ sDesc.depthAttachmentPixelFormat = MTLPixelFormatDepth32Float;
446
+ sDesc.rasterSampleCount = (NSUInteger)std::max(1, sampleCount_);
447
+ skyPipeline_ = (__bridge_retained void*)
448
+ [dev newRenderPipelineStateWithDescriptor:sDesc error:&err];
449
+ if (err) NSLog(@"[MetalBackend] sky pipeline error: %@", err);
450
+
451
+ if (!terrainDepthState_) {
452
+ MTLDepthStencilDescriptor* dd = [MTLDepthStencilDescriptor new];
453
+ dd.depthCompareFunction = MTLCompareFunctionGreater;
454
+ dd.depthWriteEnabled = YES;
455
+ terrainDepthState_ = (__bridge_retained void*)
456
+ [dev newDepthStencilStateWithDescriptor:dd];
457
+ }
458
+ if (!skyDepthState_) {
459
+ MTLDepthStencilDescriptor* sd = [MTLDepthStencilDescriptor new];
460
+ sd.depthCompareFunction = MTLCompareFunctionAlways;
461
+ sd.depthWriteEnabled = NO;
462
+ skyDepthState_ = (__bridge_retained void*)
463
+ [dev newDepthStencilStateWithDescriptor:sd];
464
+ }
465
+ }
466
+
467
+ void MetalBackend::setMsaaSampleCount(int sc) {
468
+ int n = sc;
469
+ if (n != 2 && n != 4) n = 1;
470
+ id<MTLDevice> dev = (__bridge id<MTLDevice>)device_;
471
+ if (dev && n > 1 && ![dev supportsTextureSampleCount:(NSUInteger)n]) n = 1;
472
+ if (n == sampleCount_) return;
473
+
474
+ if (frameSemaphore_) {
475
+ dispatch_semaphore_t sem = (__bridge dispatch_semaphore_t)frameSemaphore_;
476
+ for (int i = 0; i < kMaxFramesInFlight; ++i) {
477
+ dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
478
+ }
479
+ }
480
+
481
+ sampleCount_ = n;
482
+ buildRenderPipelines();
483
+ createDepthTexture();
484
+
485
+ if (frameSemaphore_) {
486
+ dispatch_semaphore_t sem = (__bridge dispatch_semaphore_t)frameSemaphore_;
487
+ for (int i = 0; i < kMaxFramesInFlight; ++i) {
488
+ dispatch_semaphore_signal(sem);
489
+ }
490
+ }
491
+ }
492
+
493
+ void MetalBackend::resize(int width, int height) {
494
+ viewportWidth_ = width;
495
+ viewportHeight_ = height;
496
+ createDepthTexture();
497
+ }
498
+
499
+ void MetalBackend::shutdown() {
500
+ // Drain in-flight frames: wait until all GPU completion handlers have fired
501
+ // before releasing the semaphore and the persistent buffers they reference.
502
+ if (frameSemaphore_) {
503
+ dispatch_semaphore_t sem = (__bridge dispatch_semaphore_t)frameSemaphore_;
504
+ for (int i = 0; i < kMaxFramesInFlight; ++i) {
505
+ dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
506
+ }
507
+ CFRelease(frameSemaphore_);
508
+ frameSemaphore_ = nullptr;
509
+ }
510
+
511
+ for (int i = 0; i < kMaxFramesInFlight; ++i) {
512
+ if (vtxBufs_[i]) { CFRelease(vtxBufs_[i]); vtxBufs_[i] = nullptr; }
513
+ if (idxBufs_[i]) { CFRelease(idxBufs_[i]); idxBufs_[i] = nullptr; }
514
+ if (uvBufs_[i]) { CFRelease(uvBufs_[i]); uvBufs_[i] = nullptr; }
515
+ if (altBufs_[i]) { CFRelease(altBufs_[i]); altBufs_[i] = nullptr; }
516
+ vtxCaps_[i] = idxCaps_[i] = uvCaps_[i] = altCaps_[i] = 0;
517
+ }
518
+
519
+ if (device_) { CFRelease(device_); device_ = nullptr; }
520
+ if (commandQueue_) { CFRelease(commandQueue_); commandQueue_ = nullptr; }
521
+ if (terrainPipeline_) { CFRelease(terrainPipeline_); terrainPipeline_ = nullptr; }
522
+ if (skyPipeline_) { CFRelease(skyPipeline_); skyPipeline_ = nullptr; }
523
+ if (terrainDepthState_) { CFRelease(terrainDepthState_); terrainDepthState_ = nullptr; }
524
+ if (skyDepthState_) { CFRelease(skyDepthState_); skyDepthState_ = nullptr; }
525
+ if (depthTexture_) { CFRelease(depthTexture_); depthTexture_ = nullptr; }
526
+ if (fallbackTexture_) { CFRelease(fallbackTexture_); fallbackTexture_ = nullptr; }
527
+ if (fallbackWaterMaskTexture_) { CFRelease(fallbackWaterMaskTexture_); fallbackWaterMaskTexture_= nullptr; }
528
+ }
529
+
530
+ void MetalBackend::createDepthTexture() {
531
+ if (viewportWidth_ <= 0 || viewportHeight_ <= 0 || !device_) return;
532
+ id<MTLDevice> dev = (__bridge id<MTLDevice>)device_;
533
+
534
+ if (depthTexture_) { CFRelease(depthTexture_); depthTexture_ = nullptr; }
535
+
536
+ MTLTextureDescriptor* dd =
537
+ [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatDepth32Float
538
+ width:viewportWidth_
539
+ height:viewportHeight_
540
+ mipmapped:NO];
541
+ const int sc = std::max(1, sampleCount_);
542
+ if (sc > 1) {
543
+ dd.textureType = MTLTextureType2DMultisample;
544
+ dd.sampleCount = (NSUInteger)sc;
545
+ }
546
+ dd.storageMode = MTLStorageModePrivate;
547
+ dd.usage = MTLTextureUsageRenderTarget;
548
+ depthTexture_ = (__bridge_retained void*)[dev newTextureWithDescriptor:dd];
549
+ }
550
+
551
+ void MetalBackend::createFallbackTexture() {
552
+ if (!device_) return;
553
+ id<MTLDevice> dev = (__bridge id<MTLDevice>)device_;
554
+
555
+ MTLTextureDescriptor* td =
556
+ [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
557
+ width:1
558
+ height:1
559
+ mipmapped:NO];
560
+ td.usage = MTLTextureUsageShaderRead;
561
+ td.storageMode = MTLStorageModeShared;
562
+ id<MTLTexture> tex = [dev newTextureWithDescriptor:td];
563
+
564
+ const uint8_t white[4] = {255, 255, 255, 255};
565
+ [tex replaceRegion:MTLRegionMake2D(0, 0, 1, 1)
566
+ mipmapLevel:0
567
+ withBytes:white
568
+ bytesPerRow:4];
569
+ fallbackTexture_ = (__bridge_retained void*)tex;
570
+ }
571
+
572
+ void MetalBackend::createWaterMaskFallback() {
573
+ if (!device_) return;
574
+ id<MTLDevice> dev = (__bridge id<MTLDevice>)device_;
575
+
576
+ if (fallbackWaterMaskTexture_) {
577
+ CFRelease(fallbackWaterMaskTexture_);
578
+ fallbackWaterMaskTexture_ = nullptr;
579
+ }
580
+
581
+ MTLTextureDescriptor* td =
582
+ [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
583
+ width:1
584
+ height:1
585
+ mipmapped:NO];
586
+ td.usage = MTLTextureUsageShaderRead;
587
+ td.storageMode = MTLStorageModeShared;
588
+ id<MTLTexture> tex = [dev newTextureWithDescriptor:td];
589
+
590
+ // All-zero = land; fwidth(0.0) = 0.0 so coastA = 0 for solid-land tiles.
591
+ const uint8_t zero[4] = {0, 0, 0, 255};
592
+ [tex replaceRegion:MTLRegionMake2D(0, 0, 1, 1)
593
+ mipmapLevel:0
594
+ withBytes:zero
595
+ bytesPerRow:4];
596
+ fallbackWaterMaskTexture_ = (__bridge_retained void*)tex;
597
+ }
598
+
599
+ void* MetalBackend::createRasterTexture(const uint8_t* pixels,
600
+ int32_t width,
601
+ int32_t height) {
602
+ if (!device_ || !pixels || width <= 0 || height <= 0) return nullptr;
603
+ id<MTLDevice> dev = (__bridge id<MTLDevice>)device_;
604
+
605
+ MTLTextureDescriptor* td =
606
+ [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
607
+ width:(NSUInteger)width
608
+ height:(NSUInteger)height
609
+ mipmapped:NO];
610
+ td.usage = MTLTextureUsageShaderRead;
611
+ td.storageMode = MTLStorageModeShared;
612
+
613
+ id<MTLTexture> tex = [dev newTextureWithDescriptor:td];
614
+ if (!tex) return nullptr;
615
+
616
+ [tex replaceRegion:MTLRegionMake2D(0, 0, (NSUInteger)width, (NSUInteger)height)
617
+ mipmapLevel:0
618
+ withBytes:pixels
619
+ bytesPerRow:(NSUInteger)(width * 4)];
620
+
621
+ return (__bridge_retained void*)tex;
622
+ }
623
+
624
+ // ── Per-frame ─────────────────────────────────────────────────────────────────
625
+
626
+ void MetalBackend::beginFrame(const FrameParams& /*params*/) {
627
+ // Acquire a frame slot. Blocks if kMaxFramesInFlight frames are already
628
+ // queued on the GPU. The completion handler in endFrame() signals the
629
+ // semaphore to release the slot.
630
+ dispatch_semaphore_wait((__bridge dispatch_semaphore_t)frameSemaphore_,
631
+ DISPATCH_TIME_FOREVER);
632
+ frameIndex_ = (frameIndex_ + 1) % kMaxFramesInFlight;
633
+
634
+ CAMetalLayer* layer = (__bridge CAMetalLayer*)metalLayer_;
635
+ id<MTLCommandQueue> cq = (__bridge id<MTLCommandQueue>)commandQueue_;
636
+
637
+ // Sync depth texture with actual drawable size if it changed.
638
+ CGSize drawSize = layer.drawableSize;
639
+ int dw = (int)drawSize.width, dh = (int)drawSize.height;
640
+ if (dw > 0 && dh > 0) {
641
+ bool needsCreate = !depthTexture_;
642
+ if (depthTexture_) {
643
+ id<MTLTexture> dt = (__bridge id<MTLTexture>)depthTexture_;
644
+ needsCreate = ((int)dt.width != dw || (int)dt.height != dh ||
645
+ (int)dt.sampleCount != std::max(1, sampleCount_));
646
+ }
647
+ if (needsCreate) {
648
+ viewportWidth_ = dw;
649
+ viewportHeight_ = dh;
650
+ createDepthTexture();
651
+ }
652
+ }
653
+
654
+ id<CAMetalDrawable> drawable = [layer nextDrawable];
655
+ if (!drawable) {
656
+ // No drawable available — restore semaphore slot and bail.
657
+ dispatch_semaphore_signal((__bridge dispatch_semaphore_t)frameSemaphore_);
658
+ currentDrawable_ = nullptr;
659
+ currentCommandBuffer_ = nullptr;
660
+ return;
661
+ }
662
+ currentDrawable_ = (__bridge_retained void*)drawable;
663
+
664
+ id<MTLCommandBuffer> cb = [cq commandBuffer];
665
+ currentCommandBuffer_ = (__bridge_retained void*)cb;
666
+
667
+ MTLRenderPassDescriptor* rp = [MTLRenderPassDescriptor new];
668
+ rp.colorAttachments[0].texture = drawable.texture;
669
+ rp.colorAttachments[0].loadAction = MTLLoadActionClear;
670
+ rp.colorAttachments[0].storeAction = MTLStoreActionStore;
671
+ rp.colorAttachments[0].clearColor = MTLClearColorMake(0, 0, 0, 1);
672
+
673
+ if (depthTexture_) {
674
+ rp.depthAttachment.texture = (__bridge id<MTLTexture>)depthTexture_;
675
+ rp.depthAttachment.loadAction = MTLLoadActionClear;
676
+ rp.depthAttachment.storeAction = MTLStoreActionDontCare;
677
+ rp.depthAttachment.clearDepth = 0.0; // reversed-Z: 0 = far
678
+ }
679
+
680
+ id<MTLRenderCommandEncoder> enc =
681
+ [cb renderCommandEncoderWithDescriptor:rp];
682
+ currentEncoder_ = (__bridge_retained void*)enc;
683
+ }
684
+
685
+ void MetalBackend::drawScene(const FrameResult& frame) {
686
+ if (!currentEncoder_ || !currentCommandBuffer_ || !currentDrawable_) return;
687
+
688
+ id<MTLRenderCommandEncoder> enc = (__bridge id<MTLRenderCommandEncoder>)currentEncoder_;
689
+ id<MTLDevice> dev = (__bridge id<MTLDevice>)device_;
690
+
691
+ // ── Sky pass ──────────────────────────────────────────────────────────────
692
+ if (skyPipeline_ && skyDepthState_) {
693
+ SkyUniforms skyU{};
694
+ const float* iv = glm::value_ptr(frame.invVP);
695
+ for (int i = 0; i < 16; ++i) skyU.invVP[i] = iv[i];
696
+ skyU.cameraEcef[0] = frame.cameraEcef.x;
697
+ skyU.cameraEcef[1] = frame.cameraEcef.y;
698
+ skyU.cameraEcef[2] = frame.cameraEcef.z;
699
+ float cl = sqrtf(frame.cameraEcef.x * frame.cameraEcef.x +
700
+ frame.cameraEcef.y * frame.cameraEcef.y +
701
+ frame.cameraEcef.z * frame.cameraEcef.z);
702
+ if (cl > 0.0f) {
703
+ skyU.lightDir[0] = frame.cameraEcef.x / cl;
704
+ skyU.lightDir[1] = frame.cameraEcef.y / cl;
705
+ skyU.lightDir[2] = frame.cameraEcef.z / cl;
706
+ }
707
+
708
+ [enc setRenderPipelineState:(__bridge id<MTLRenderPipelineState>)skyPipeline_];
709
+ [enc setDepthStencilState:(__bridge id<MTLDepthStencilState>)skyDepthState_];
710
+ [enc setCullMode:MTLCullModeNone];
711
+ [enc setVertexBytes:&skyU length:sizeof(skyU) atIndex:0];
712
+ [enc setFragmentBytes:&skyU length:sizeof(skyU) atIndex:0];
713
+ [enc drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
714
+ }
715
+
716
+ // ── Terrain pass ──────────────────────────────────────────────────────────
717
+ if (frame.draws.empty() || frame.localPositions.empty() || frame.indices.empty() ||
718
+ !terrainPipeline_ || !terrainDepthState_) {
719
+ return;
720
+ }
721
+
722
+ const int fi = frameIndex_;
723
+
724
+ // Upload merged geometry to the current frame's persistent Metal buffers.
725
+ const size_t vtxBytes = frame.localPositions.size() * sizeof(float);
726
+ const size_t idxBytes = frame.indices.size() * sizeof(uint32_t);
727
+ const size_t uvBytes = frame.uvs.size() * sizeof(float);
728
+ const size_t altBytes = frame.altitudes.size() * sizeof(float);
729
+
730
+ id<MTLBuffer> vtxBuf = ensureBuffer(&vtxBufs_[fi], &vtxCaps_[fi],
731
+ vtxBytes, frame.localPositions.data(), dev);
732
+ id<MTLBuffer> idxBuf = ensureBuffer(&idxBufs_[fi], &idxCaps_[fi],
733
+ idxBytes, frame.indices.data(), dev);
734
+ id<MTLBuffer> uvBuf = ensureBuffer(&uvBufs_[fi], &uvCaps_[fi],
735
+ uvBytes, frame.uvs.data(), dev);
736
+ id<MTLBuffer> altBuf = ensureBuffer(&altBufs_[fi], &altCaps_[fi],
737
+ altBytes, frame.altitudes.data(), dev);
738
+
739
+ if (!vtxBuf || !idxBuf || !altBuf) return;
740
+
741
+ [enc setRenderPipelineState:(__bridge id<MTLRenderPipelineState>)terrainPipeline_];
742
+ [enc setDepthStencilState:(__bridge id<MTLDepthStencilState>)terrainDepthState_];
743
+ [enc setFrontFacingWinding:MTLWindingCounterClockwise];
744
+ [enc setCullMode:MTLCullModeBack];
745
+
746
+ // Set vertex buffers that are shared across all draws.
747
+ // buffer(0): tile-local positions, buffer(1): altitudes, buffer(3): UVs.
748
+ // buffer(2) is the per-draw MVP set in the loop below.
749
+ [enc setVertexBuffer:vtxBuf offset:0 atIndex:0];
750
+ [enc setVertexBuffer:altBuf offset:0 atIndex:1];
751
+ if (uvBuf) [enc setVertexBuffer:uvBuf offset:0 atIndex:3];
752
+
753
+ id<MTLTexture> fallbackTex = (__bridge id<MTLTexture>)fallbackTexture_;
754
+ id<MTLTexture> fallbackWMTex = (__bridge id<MTLTexture>)fallbackWaterMaskTexture_;
755
+
756
+ if (fallbackWMTex) [enc setFragmentTexture:fallbackWMTex atIndex:1];
757
+
758
+ for (const auto& draw : frame.draws) {
759
+ if (draw.indexCount == 0) continue;
760
+
761
+ // ── Per-draw vertex uniforms: the tile's RTC MVP matrix ───────────────
762
+ PerDrawVertexUniformsCPP vdu{};
763
+ const float* mp = glm::value_ptr(draw.mvpMatrix);
764
+ for (int i = 0; i < 16; ++i) vdu.mvpMatrix[i] = mp[i];
765
+ [enc setVertexBytes:&vdu length:sizeof(vdu) atIndex:2];
766
+
767
+ // ── Per-draw fragment uniforms ─────────────────────────────────────────
768
+ OverlayParamsCPP ov{};
769
+ ov.hasOverlay = (draw.overlayTexture && draw.hasUVs) ? 1u : 0u;
770
+ ov.isEllipsoidFallback = draw.isEllipsoidFallback ? 1u : 0u;
771
+ ov.isOnlyWater = draw.isOnlyWater ? 1u : 0u;
772
+ ov.hasWaterMask = draw.waterMaskTexture ? 1u : 0u;
773
+ ov.translation[0] = draw.overlayTranslation.x;
774
+ ov.translation[1] = draw.overlayTranslation.y;
775
+ ov.scale[0] = draw.overlayScale.x;
776
+ ov.scale[1] = draw.overlayScale.y;
777
+ ov.wmWest = draw.wmTileBounds.x;
778
+ ov.wmSouth = draw.wmTileBounds.y;
779
+ ov.wmEast = draw.wmTileBounds.z;
780
+ ov.wmNorth = draw.wmTileBounds.w;
781
+ ov.wmTransX = draw.wmTranslation.x;
782
+ ov.wmTransY = draw.wmTranslation.y;
783
+ ov.wmScale = draw.wmScale;
784
+ // RTC: tile centre and camera-in-tile-space for fragment world-position
785
+ // reconstruction and view-direction lighting.
786
+ ov.rtcCenterX = draw.rtcCenterEcef.x;
787
+ ov.rtcCenterY = draw.rtcCenterEcef.y;
788
+ ov.rtcCenterZ = draw.rtcCenterEcef.z;
789
+ // cameraTilespace = cameraEcef − rtcCentre. Calculated here to keep the
790
+ // fragment shader free of subtraction of two large float3 values.
791
+ ov.cameraTilespaceX = frame.cameraEcef.x - draw.rtcCenterEcef.x;
792
+ ov.cameraTilespaceY = frame.cameraEcef.y - draw.rtcCenterEcef.y;
793
+ ov.cameraTilespaceZ = frame.cameraEcef.z - draw.rtcCenterEcef.z;
794
+ [enc setFragmentBytes:&ov length:sizeof(ov) atIndex:0];
795
+
796
+ id<MTLTexture> tex = (draw.overlayTexture && draw.hasUVs)
797
+ ? (__bridge id<MTLTexture>)draw.overlayTexture
798
+ : fallbackTex;
799
+ if (tex) [enc setFragmentTexture:tex atIndex:0];
800
+
801
+ id<MTLTexture> wmTex = draw.waterMaskTexture
802
+ ? (__bridge id<MTLTexture>)draw.waterMaskTexture
803
+ : fallbackWMTex;
804
+ if (wmTex) [enc setFragmentTexture:wmTex atIndex:1];
805
+
806
+ [enc drawIndexedPrimitives:MTLPrimitiveTypeTriangle
807
+ indexCount:draw.indexCount
808
+ indexType:MTLIndexTypeUInt32
809
+ indexBuffer:idxBuf
810
+ indexBufferOffset:draw.indexByteOffset];
811
+ }
812
+ }
813
+
814
+ void MetalBackend::endFrame() {
815
+ if (!currentCommandBuffer_ || !currentDrawable_) return;
816
+
817
+ id<MTLRenderCommandEncoder> enc =
818
+ (__bridge_transfer id<MTLRenderCommandEncoder>)currentEncoder_;
819
+ [enc endEncoding];
820
+ currentEncoder_ = nullptr;
821
+
822
+ id<CAMetalDrawable> drawable =
823
+ (__bridge_transfer id<CAMetalDrawable>)currentDrawable_;
824
+ id<MTLCommandBuffer> cb =
825
+ (__bridge_transfer id<MTLCommandBuffer>)currentCommandBuffer_;
826
+
827
+ // Signal the frame semaphore when the GPU is done with this frame's buffers.
828
+ void* semPtr = frameSemaphore_;
829
+ [cb addCompletedHandler:^(id<MTLCommandBuffer> __unused) {
830
+ dispatch_semaphore_signal((__bridge dispatch_semaphore_t)semPtr);
831
+ }];
832
+
833
+ [cb presentDrawable:drawable];
834
+ [cb commit];
835
+ currentDrawable_ = nullptr;
836
+ currentCommandBuffer_ = nullptr;
837
+ }
838
+
839
+ } // namespace reactnativecesium