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,1781 @@
1
+ // VulkanBackend.cpp — Android Vulkan rendering backend
2
+ //
3
+ // Architecture mirrors MetalBackend.mm:
4
+ // - RTC (Relative-to-Center) vertex positions: tile-local float offsets from
5
+ // each tile's ECEF centre, computed on the CPU.
6
+ // - Per-tile MVP matrix computed on the CPU in double precision (baking in the
7
+ // large camera↔tile-centre translation), then cast to float32 for the GPU.
8
+ // - Pre-computed vertex altitudes (ellipsoid height metres, double-precision
9
+ // Bowring formula on CPU), passed as a separate vertex attribute to avoid
10
+ // catastrophic cancellation in the shader.
11
+ // - Single merged vertex + index buffer uploaded each frame.
12
+ // - Reversed-Z infinite projection: depth clear = 0, compare = GREATER.
13
+ // - Sky drawn first (no depth write), terrain drawn after.
14
+ // - Water mask: UV computed from geographic lat/lon + tile bounds (not fragUV).
15
+ // - Push constants split: bytes 0-63 vertex (MVP), bytes 64-127 fragment.
16
+ // - Triple-buffered persistent VkBuffers guarded by VkFence.
17
+
18
+ #include "VulkanBackend.h"
19
+
20
+ #include <android/native_window.h>
21
+ #include <android/log.h>
22
+
23
+ #include <glm/gtc/type_ptr.hpp>
24
+
25
+ #include <algorithm>
26
+ #include <array>
27
+ #include <cassert>
28
+ #include <cstring>
29
+ #include <deque>
30
+ #include <mutex>
31
+ #include <stdexcept>
32
+
33
+ // Pre-compiled SPIR-V headers (generated at build time by glslc + spv_to_header.cmake)
34
+ #include "terrain.vert.spv.h"
35
+ #include "terrain.frag.spv.h"
36
+ #include "sky.vert.spv.h"
37
+ #include "sky.frag.spv.h"
38
+
39
+ #define LOG_TAG "VulkanBackend"
40
+ #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
41
+ #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
42
+
43
+ #define VK_CHECK(call) do { \
44
+ VkResult _r = (call); \
45
+ if (_r != VK_SUCCESS) { \
46
+ LOGE("%s failed: %d at %s:%d", #call, _r, __FILE__, __LINE__); \
47
+ } \
48
+ } while(0)
49
+
50
+ // Per-frame UBO (set 0): camera ECEF for the fragment lighting view direction.
51
+ // MVP is now per-draw via push constants; only cameraEcef stays in the UBO.
52
+ struct TerrainUBO {
53
+ float cameraEcef[4]; // 16 bytes
54
+ };
55
+
56
+ struct SkyUBO {
57
+ float invVP[16]; // 64 bytes
58
+ float cameraEcef[4]; // 16 bytes
59
+ float lightDir[4]; // 16 bytes
60
+ }; // 96 bytes
61
+
62
+ // Push constants split across vertex (bytes 0-63) and fragment (bytes 64-127).
63
+ // Total = 128 bytes (the Vulkan spec minimum guarantee on all devices).
64
+ struct TerrainVertexPC {
65
+ float mvpMatrix[16]; // 64 bytes — per-tile RTC MVP (double-precision source)
66
+ };
67
+
68
+ struct TerrainFragmentPC {
69
+ // Fragment push constants occupy bytes 64-127 in the push constant block.
70
+ uint32_t hasOverlay; // 4 bytes
71
+ uint32_t isEllipsoidFallback; // 4 bytes
72
+ uint32_t isOnlyWater; // 4 bytes
73
+ uint32_t hasWaterMask; // 4 bytes
74
+ float wmWest; // 4 bytes — tile geographic bounds (radians)
75
+ float wmSouth; // 4 bytes
76
+ float wmEast; // 4 bytes
77
+ float wmNorth; // 4 bytes
78
+ float rtcCenterX; // 4 bytes — tile RTC centre ECEF (float)
79
+ float rtcCenterY; // 4 bytes
80
+ float rtcCenterZ; // 4 bytes
81
+ float translationX; // 4 bytes — overlay UV transform
82
+ float translationY; // 4 bytes
83
+ float scaleX; // 4 bytes
84
+ float scaleY; // 4 bytes
85
+ float _pad; // 4 bytes
86
+ }; // 64 bytes
87
+
88
+ namespace reactnativecesium {
89
+
90
+ VulkanBackend::VulkanBackend() = default;
91
+
92
+ VulkanBackend::~VulkanBackend() { shutdown(); }
93
+
94
+ void VulkanBackend::initialize(void* nativeSurface, int width, int height) {
95
+ window_ = nativeSurface;
96
+ viewportWidth_ = width;
97
+ viewportHeight_ = height;
98
+
99
+ createInstance();
100
+ VkAndroidSurfaceCreateInfoKHR surfaceInfo{};
101
+ surfaceInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR;
102
+ surfaceInfo.window = static_cast<ANativeWindow*>(window_);
103
+ VK_CHECK(vkCreateAndroidSurfaceKHR(instance_, &surfaceInfo, nullptr, &surface_));
104
+
105
+ pickPhysicalDevice();
106
+ createDevice();
107
+ createCommandPool();
108
+ createSwapchain();
109
+ createRenderPass();
110
+ createDepthResources();
111
+ createFramebuffers();
112
+ createCommandBuffers();
113
+ createSyncObjects();
114
+ createDescriptorSetLayout();
115
+ createDescriptorPool();
116
+ createWaterMaskPool();
117
+ createPipelineLayout();
118
+ createSkyPipeline();
119
+ createTerrainPipeline();
120
+ createFallbackTexture();
121
+ createWaterMaskFallback();
122
+
123
+ // Allocate sky UBO + descriptor set (persistently mapped)
124
+ createBuffer(sizeof(SkyUBO), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
125
+ VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
126
+ skyUboBuffer_, skyUboMemory_);
127
+ VK_CHECK(vkMapMemory(device_, skyUboMemory_, 0, sizeof(SkyUBO), 0, &skyUboMapped_));
128
+
129
+ VkDescriptorSetAllocateInfo skyAllocInfo{};
130
+ skyAllocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
131
+ skyAllocInfo.descriptorPool = descriptorPool_;
132
+ skyAllocInfo.descriptorSetCount = 1;
133
+ skyAllocInfo.pSetLayouts = &skyDescSetLayout_;
134
+ VK_CHECK(vkAllocateDescriptorSets(device_, &skyAllocInfo, &skyDescSet_));
135
+
136
+ VkDescriptorBufferInfo skyBufInfo{};
137
+ skyBufInfo.buffer = skyUboBuffer_;
138
+ skyBufInfo.offset = 0;
139
+ skyBufInfo.range = sizeof(SkyUBO);
140
+
141
+ VkWriteDescriptorSet skyWrite{};
142
+ skyWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
143
+ skyWrite.dstSet = skyDescSet_;
144
+ skyWrite.dstBinding = 0;
145
+ skyWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
146
+ skyWrite.descriptorCount = 1;
147
+ skyWrite.pBufferInfo = &skyBufInfo;
148
+ vkUpdateDescriptorSets(device_, 1, &skyWrite, 0, nullptr);
149
+
150
+ // Allocate terrain UBOs + descriptor sets (set 0: UBO only, one per frame)
151
+ for (int i = 0; i < kMaxFramesInFlight; ++i) {
152
+ createBuffer(sizeof(TerrainUBO), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
153
+ VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
154
+ terrainUboBufs_[i], terrainUboMems_[i]);
155
+
156
+ VkDescriptorSetAllocateInfo allocInfo{};
157
+ allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
158
+ allocInfo.descriptorPool = descriptorPool_;
159
+ allocInfo.descriptorSetCount = 1;
160
+ allocInfo.pSetLayouts = &terrainDescSetLayout_;
161
+ VK_CHECK(vkAllocateDescriptorSets(device_, &allocInfo, &terrainDescSets_[i]));
162
+
163
+ VkDescriptorBufferInfo bufInfo{};
164
+ bufInfo.buffer = terrainUboBufs_[i];
165
+ bufInfo.offset = 0;
166
+ bufInfo.range = sizeof(TerrainUBO);
167
+
168
+ VkWriteDescriptorSet uboWrite{};
169
+ uboWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
170
+ uboWrite.dstSet = terrainDescSets_[i];
171
+ uboWrite.dstBinding = 0;
172
+ uboWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
173
+ uboWrite.descriptorCount = 1;
174
+ uboWrite.pBufferInfo = &bufInfo;
175
+ vkUpdateDescriptorSets(device_, 1, &uboWrite, 0, nullptr);
176
+
177
+ // Persistent mapping — HOST_COHERENT so no explicit flush needed.
178
+ VK_CHECK(vkMapMemory(device_, terrainUboMems_[i], 0, sizeof(TerrainUBO),
179
+ 0, &terrainUboMapped_[i]));
180
+ }
181
+
182
+ // Allocate fallback texture descriptor set (set 1)
183
+ {
184
+ VkDescriptorSetAllocateInfo allocInfo{};
185
+ allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
186
+ allocInfo.descriptorPool = descriptorPool_;
187
+ allocInfo.descriptorSetCount = 1;
188
+ allocInfo.pSetLayouts = &terrainTexLayout_;
189
+ VK_CHECK(vkAllocateDescriptorSets(device_, &allocInfo, &fallbackTexDescSet_));
190
+
191
+ VkDescriptorImageInfo imgInfo{};
192
+ imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
193
+ imgInfo.imageView = fallbackTexture_.imageView;
194
+ imgInfo.sampler = fallbackTexture_.sampler;
195
+
196
+ VkWriteDescriptorSet texWrite{};
197
+ texWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
198
+ texWrite.dstSet = fallbackTexDescSet_;
199
+ texWrite.dstBinding = 0;
200
+ texWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
201
+ texWrite.descriptorCount = 1;
202
+ texWrite.pImageInfo = &imgInfo;
203
+ vkUpdateDescriptorSets(device_, 1, &texWrite, 0, nullptr);
204
+ }
205
+
206
+ LOGI("VulkanBackend initialized: %dx%d, %zu swapchain images",
207
+ width, height, swapchainImages_.size());
208
+ }
209
+
210
+ void VulkanBackend::resize(int width, int height) {
211
+ viewportWidth_ = width;
212
+ viewportHeight_ = height;
213
+ needsSwapchainRecreate_ = true;
214
+ }
215
+
216
+ void VulkanBackend::shutdown() {
217
+ if (device_ == VK_NULL_HANDLE) return;
218
+ vkDeviceWaitIdle(device_);
219
+
220
+ // Drain the deferred deletion queue — GPU is idle so all are safe to free.
221
+ {
222
+ std::lock_guard<std::mutex> lk(pendingDeletesMutex_);
223
+ while (!pendingDeletes_.empty()) {
224
+ auto& front = pendingDeletes_.front();
225
+ destroyTexture(front.tex, front.pool);
226
+ pendingDeletes_.pop_front();
227
+ }
228
+ }
229
+
230
+ // Free fallback imagery texture
231
+ if (fallbackTexture_.sampler) vkDestroySampler(device_, fallbackTexture_.sampler, nullptr);
232
+ if (fallbackTexture_.imageView) vkDestroyImageView(device_, fallbackTexture_.imageView, nullptr);
233
+ if (fallbackTexture_.image) vkDestroyImage(device_, fallbackTexture_.image, nullptr);
234
+ if (fallbackTexture_.memory) vkFreeMemory(device_, fallbackTexture_.memory, nullptr);
235
+ fallbackTexture_ = {};
236
+
237
+ // Free fallback water mask texture (its descriptor set belongs to waterMaskPool_)
238
+ if (fallbackWaterMaskTex_.sampler) vkDestroySampler(device_, fallbackWaterMaskTex_.sampler, nullptr);
239
+ if (fallbackWaterMaskTex_.imageView) vkDestroyImageView(device_, fallbackWaterMaskTex_.imageView, nullptr);
240
+ if (fallbackWaterMaskTex_.image) vkDestroyImage(device_, fallbackWaterMaskTex_.image, nullptr);
241
+ if (fallbackWaterMaskTex_.memory) vkFreeMemory(device_, fallbackWaterMaskTex_.memory, nullptr);
242
+ fallbackWaterMaskTex_ = {};
243
+ fallbackWaterMaskDescSet_ = VK_NULL_HANDLE;
244
+
245
+ // Free sky UBO (unmap before destroy)
246
+ if (skyUboMapped_) { vkUnmapMemory(device_, skyUboMemory_); skyUboMapped_ = nullptr; }
247
+ if (skyUboBuffer_) vkDestroyBuffer(device_, skyUboBuffer_, nullptr);
248
+ if (skyUboMemory_) vkFreeMemory(device_, skyUboMemory_, nullptr);
249
+ skyUboBuffer_ = VK_NULL_HANDLE;
250
+ skyUboMemory_ = VK_NULL_HANDLE;
251
+
252
+ // Free terrain UBOs (unmap before destroy)
253
+ for (int i = 0; i < kMaxFramesInFlight; ++i) {
254
+ if (terrainUboMapped_[i]) { vkUnmapMemory(device_, terrainUboMems_[i]); terrainUboMapped_[i] = nullptr; }
255
+ if (terrainUboBufs_[i]) vkDestroyBuffer(device_, terrainUboBufs_[i], nullptr);
256
+ if (terrainUboMems_[i]) vkFreeMemory(device_, terrainUboMems_[i], nullptr);
257
+ terrainUboBufs_[i] = VK_NULL_HANDLE;
258
+ terrainUboMems_[i] = VK_NULL_HANDLE;
259
+ }
260
+
261
+ // Free persistent frame buffers (position, index, UV, altitude)
262
+ for (int i = 0; i < kMaxFramesInFlight; ++i) {
263
+ if (vtxBufs_[i]) vkDestroyBuffer(device_, vtxBufs_[i], nullptr);
264
+ if (vtxMems_[i]) vkFreeMemory(device_, vtxMems_[i], nullptr);
265
+ if (idxBufs_[i]) vkDestroyBuffer(device_, idxBufs_[i], nullptr);
266
+ if (idxMems_[i]) vkFreeMemory(device_, idxMems_[i], nullptr);
267
+ if (uvBufs_[i]) vkDestroyBuffer(device_, uvBufs_[i], nullptr);
268
+ if (uvMems_[i]) vkFreeMemory(device_, uvMems_[i], nullptr);
269
+ if (altBufs_[i]) vkDestroyBuffer(device_, altBufs_[i], nullptr);
270
+ if (altMems_[i]) vkFreeMemory(device_, altMems_[i], nullptr);
271
+ vtxBufs_[i] = idxBufs_[i] = uvBufs_[i] = altBufs_[i] = VK_NULL_HANDLE;
272
+ vtxMems_[i] = idxMems_[i] = uvMems_[i] = altMems_[i] = VK_NULL_HANDLE;
273
+ vtxCaps_[i] = idxCaps_[i] = uvCaps_[i] = altCaps_[i] = 0;
274
+ }
275
+
276
+ for (auto& s : imageAvailableSemaphores_) vkDestroySemaphore(device_, s, nullptr);
277
+ for (auto& s : renderFinishedSemaphores_) vkDestroySemaphore(device_, s, nullptr);
278
+ for (auto& f : inFlightFences_) vkDestroyFence(device_, f, nullptr);
279
+ imageAvailableSemaphores_.clear();
280
+ renderFinishedSemaphores_.clear();
281
+ inFlightFences_.clear();
282
+
283
+ cleanupSwapchain();
284
+
285
+ if (terrainPipeline_) vkDestroyPipeline(device_, terrainPipeline_, nullptr);
286
+ if (skyPipeline_) vkDestroyPipeline(device_, skyPipeline_, nullptr);
287
+ if (terrainPipelineLayout_) vkDestroyPipelineLayout(device_, terrainPipelineLayout_, nullptr);
288
+ if (skyPipelineLayout_) vkDestroyPipelineLayout(device_, skyPipelineLayout_, nullptr);
289
+ if (terrainDescSetLayout_) vkDestroyDescriptorSetLayout(device_, terrainDescSetLayout_, nullptr);
290
+ if (terrainTexLayout_) vkDestroyDescriptorSetLayout(device_, terrainTexLayout_, nullptr);
291
+ if (skyDescSetLayout_) vkDestroyDescriptorSetLayout(device_, skyDescSetLayout_, nullptr);
292
+ if (descriptorPool_) vkDestroyDescriptorPool(device_, descriptorPool_, nullptr);
293
+ if (waterMaskPool_) vkDestroyDescriptorPool(device_, waterMaskPool_, nullptr);
294
+ if (commandPool_) vkDestroyCommandPool(device_, commandPool_, nullptr);
295
+ if (renderPass_) vkDestroyRenderPass(device_, renderPass_, nullptr);
296
+
297
+ terrainPipeline_ = skyPipeline_ = VK_NULL_HANDLE;
298
+ terrainPipelineLayout_ = skyPipelineLayout_ = VK_NULL_HANDLE;
299
+ terrainDescSetLayout_ = terrainTexLayout_ = skyDescSetLayout_ = VK_NULL_HANDLE;
300
+ descriptorPool_ = waterMaskPool_ = VK_NULL_HANDLE;
301
+ commandPool_ = VK_NULL_HANDLE;
302
+ renderPass_ = VK_NULL_HANDLE;
303
+
304
+ vkDestroyDevice(device_, nullptr);
305
+ device_ = VK_NULL_HANDLE;
306
+
307
+ if (surface_) vkDestroySurfaceKHR(instance_, surface_, nullptr);
308
+ if (instance_) vkDestroyInstance(instance_, nullptr);
309
+ surface_ = VK_NULL_HANDLE;
310
+ instance_ = VK_NULL_HANDLE;
311
+ }
312
+
313
+ // ── Instance + Device ──────────────────────────────────────────────────────────
314
+
315
+ void VulkanBackend::createInstance() {
316
+ VkApplicationInfo appInfo{};
317
+ appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
318
+ appInfo.pApplicationName = "ReactNativeCesium";
319
+ appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
320
+ appInfo.pEngineName = "ReactNativeCesium";
321
+ appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
322
+ appInfo.apiVersion = VK_API_VERSION_1_1;
323
+
324
+ const char* extensions[] = {
325
+ VK_KHR_SURFACE_EXTENSION_NAME,
326
+ VK_KHR_ANDROID_SURFACE_EXTENSION_NAME,
327
+ };
328
+
329
+ VkInstanceCreateInfo createInfo{};
330
+ createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
331
+ createInfo.pApplicationInfo = &appInfo;
332
+ createInfo.enabledExtensionCount = 2;
333
+ createInfo.ppEnabledExtensionNames = extensions;
334
+ VK_CHECK(vkCreateInstance(&createInfo, nullptr, &instance_));
335
+ }
336
+
337
+ void VulkanBackend::pickPhysicalDevice() {
338
+ uint32_t count = 0;
339
+ vkEnumeratePhysicalDevices(instance_, &count, nullptr);
340
+ std::vector<VkPhysicalDevice> devices(count);
341
+ vkEnumeratePhysicalDevices(instance_, &count, devices.data());
342
+
343
+ for (auto& dev : devices) {
344
+ uint32_t qCount = 0;
345
+ vkGetPhysicalDeviceQueueFamilyProperties(dev, &qCount, nullptr);
346
+ std::vector<VkQueueFamilyProperties> qFamilies(qCount);
347
+ vkGetPhysicalDeviceQueueFamilyProperties(dev, &qCount, qFamilies.data());
348
+
349
+ for (uint32_t i = 0; i < qCount; ++i) {
350
+ VkBool32 presentSupport = VK_FALSE;
351
+ vkGetPhysicalDeviceSurfaceSupportKHR(dev, i, surface_, &presentSupport);
352
+
353
+ if ((qFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) && presentSupport) {
354
+ physicalDevice_ = dev;
355
+ graphicsFamily_ = i;
356
+ return;
357
+ }
358
+ }
359
+ }
360
+ LOGE("No suitable Vulkan physical device found");
361
+ }
362
+
363
+ void VulkanBackend::createDevice() {
364
+ float queuePriority = 1.0f;
365
+ VkDeviceQueueCreateInfo queueInfo{};
366
+ queueInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
367
+ queueInfo.queueFamilyIndex = graphicsFamily_;
368
+ queueInfo.queueCount = 1;
369
+ queueInfo.pQueuePriorities = &queuePriority;
370
+
371
+ const char* devExtensions[] = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
372
+
373
+ VkPhysicalDeviceFeatures features{};
374
+
375
+ VkDeviceCreateInfo createInfo{};
376
+ createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
377
+ createInfo.queueCreateInfoCount = 1;
378
+ createInfo.pQueueCreateInfos = &queueInfo;
379
+ createInfo.enabledExtensionCount = 1;
380
+ createInfo.ppEnabledExtensionNames = devExtensions;
381
+ createInfo.pEnabledFeatures = &features;
382
+ VK_CHECK(vkCreateDevice(physicalDevice_, &createInfo, nullptr, &device_));
383
+
384
+ vkGetDeviceQueue(device_, graphicsFamily_, 0, &graphicsQueue_);
385
+ presentQueue_ = graphicsQueue_;
386
+ }
387
+
388
+ // ── Depth format selection ─────────────────────────────────────────────────────
389
+
390
+ VkFormat VulkanBackend::pickDepthFormat() const {
391
+ // Prefer D32 for maximum precision; fall back to packed D24/S8 or D16.
392
+ for (VkFormat candidate : {VK_FORMAT_D32_SFLOAT,
393
+ VK_FORMAT_D24_UNORM_S8_UINT,
394
+ VK_FORMAT_D16_UNORM}) {
395
+ VkFormatProperties props;
396
+ vkGetPhysicalDeviceFormatProperties(physicalDevice_, candidate, &props);
397
+ if (props.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT)
398
+ return candidate;
399
+ }
400
+ LOGE("No supported depth format found; defaulting to D32_SFLOAT");
401
+ return VK_FORMAT_D32_SFLOAT;
402
+ }
403
+
404
+ // ── Swapchain ──────────────────────────────────────────────────────────────────
405
+
406
+ void VulkanBackend::createSwapchain() {
407
+ VkSurfaceCapabilitiesKHR caps;
408
+ vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice_, surface_, &caps);
409
+
410
+ uint32_t fmtCount;
411
+ vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice_, surface_, &fmtCount, nullptr);
412
+ std::vector<VkSurfaceFormatKHR> formats(fmtCount);
413
+ vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice_, surface_, &fmtCount, formats.data());
414
+
415
+ // Prefer sRGB
416
+ swapchainFormat_ = formats[0].format;
417
+ VkColorSpaceKHR colorSpace = formats[0].colorSpace;
418
+ for (auto& fmt : formats) {
419
+ if (fmt.format == VK_FORMAT_B8G8R8A8_SRGB && fmt.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
420
+ swapchainFormat_ = fmt.format;
421
+ colorSpace = fmt.colorSpace;
422
+ break;
423
+ }
424
+ if (fmt.format == VK_FORMAT_R8G8B8A8_SRGB) {
425
+ swapchainFormat_ = fmt.format;
426
+ colorSpace = fmt.colorSpace;
427
+ }
428
+ }
429
+
430
+ // Determine extent
431
+ if (caps.currentExtent.width != UINT32_MAX) {
432
+ swapchainExtent_ = caps.currentExtent;
433
+ } else {
434
+ swapchainExtent_.width = std::clamp(static_cast<uint32_t>(viewportWidth_),
435
+ caps.minImageExtent.width, caps.maxImageExtent.width);
436
+ swapchainExtent_.height = std::clamp(static_cast<uint32_t>(viewportHeight_),
437
+ caps.minImageExtent.height, caps.maxImageExtent.height);
438
+ }
439
+
440
+ uint32_t imageCount = caps.minImageCount + 1;
441
+ if (caps.maxImageCount > 0 && imageCount > caps.maxImageCount)
442
+ imageCount = caps.maxImageCount;
443
+
444
+ // Pick depth format once (stable across recreations on the same device).
445
+ depthFormat_ = pickDepthFormat();
446
+
447
+ // Prefer OPAQUE composite alpha (avoids blending with the window behind the
448
+ // surface); fall back through the other modes in priority order.
449
+ VkCompositeAlphaFlagBitsKHR compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
450
+ for (auto candidate : {VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
451
+ VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR,
452
+ VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR,
453
+ VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR}) {
454
+ if (caps.supportedCompositeAlpha & candidate) {
455
+ compositeAlpha = candidate;
456
+ break;
457
+ }
458
+ }
459
+
460
+ VkSwapchainKHR oldSwapchain = swapchain_;
461
+
462
+ VkSwapchainCreateInfoKHR swapInfo{};
463
+ swapInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
464
+ swapInfo.surface = surface_;
465
+ swapInfo.minImageCount = imageCount;
466
+ swapInfo.imageFormat = swapchainFormat_;
467
+ swapInfo.imageColorSpace = colorSpace;
468
+ swapInfo.imageExtent = swapchainExtent_;
469
+ swapInfo.imageArrayLayers = 1;
470
+ swapInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
471
+ swapInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
472
+ swapInfo.preTransform = caps.currentTransform;
473
+ swapInfo.compositeAlpha = compositeAlpha;
474
+ swapInfo.presentMode = VK_PRESENT_MODE_FIFO_KHR;
475
+ swapInfo.clipped = VK_TRUE;
476
+ swapInfo.oldSwapchain = oldSwapchain;
477
+ VK_CHECK(vkCreateSwapchainKHR(device_, &swapInfo, nullptr, &swapchain_));
478
+
479
+ if (oldSwapchain != VK_NULL_HANDLE) {
480
+ vkDestroySwapchainKHR(device_, oldSwapchain, nullptr);
481
+ }
482
+
483
+ vkGetSwapchainImagesKHR(device_, swapchain_, &imageCount, nullptr);
484
+ swapchainImages_.resize(imageCount);
485
+ vkGetSwapchainImagesKHR(device_, swapchain_, &imageCount, swapchainImages_.data());
486
+
487
+ swapchainImageViews_.resize(imageCount);
488
+ for (uint32_t i = 0; i < imageCount; ++i) {
489
+ VkImageViewCreateInfo viewInfo{};
490
+ viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
491
+ viewInfo.image = swapchainImages_[i];
492
+ viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
493
+ viewInfo.format = swapchainFormat_;
494
+ viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
495
+ viewInfo.subresourceRange.baseMipLevel = 0;
496
+ viewInfo.subresourceRange.levelCount = 1;
497
+ viewInfo.subresourceRange.baseArrayLayer = 0;
498
+ viewInfo.subresourceRange.layerCount = 1;
499
+ VK_CHECK(vkCreateImageView(device_, &viewInfo, nullptr, &swapchainImageViews_[i]));
500
+ }
501
+ }
502
+
503
+ void VulkanBackend::createRenderPass() {
504
+ VkAttachmentDescription colorAttach{};
505
+ colorAttach.format = swapchainFormat_;
506
+ colorAttach.samples = VK_SAMPLE_COUNT_1_BIT;
507
+ colorAttach.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
508
+ colorAttach.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
509
+ colorAttach.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
510
+ colorAttach.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
511
+ colorAttach.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
512
+ colorAttach.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
513
+
514
+ VkAttachmentDescription depthAttach{};
515
+ depthAttach.format = depthFormat_;
516
+ depthAttach.samples = VK_SAMPLE_COUNT_1_BIT;
517
+ depthAttach.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
518
+ depthAttach.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
519
+ depthAttach.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
520
+ depthAttach.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
521
+ depthAttach.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
522
+ depthAttach.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
523
+
524
+ VkAttachmentReference colorRef{};
525
+ colorRef.attachment = 0;
526
+ colorRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
527
+
528
+ VkAttachmentReference depthRef{};
529
+ depthRef.attachment = 1;
530
+ depthRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
531
+
532
+ VkSubpassDescription subpass{};
533
+ subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
534
+ subpass.colorAttachmentCount = 1;
535
+ subpass.pColorAttachments = &colorRef;
536
+ subpass.pDepthStencilAttachment = &depthRef;
537
+
538
+ VkSubpassDependency dependency{};
539
+ dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
540
+ dependency.dstSubpass = 0;
541
+ dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
542
+ VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
543
+ dependency.srcAccessMask = 0;
544
+ dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
545
+ VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
546
+ dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
547
+ VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
548
+
549
+ std::array<VkAttachmentDescription, 2> attachments = {colorAttach, depthAttach};
550
+
551
+ VkRenderPassCreateInfo rpInfo{};
552
+ rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
553
+ rpInfo.attachmentCount = static_cast<uint32_t>(attachments.size());
554
+ rpInfo.pAttachments = attachments.data();
555
+ rpInfo.subpassCount = 1;
556
+ rpInfo.pSubpasses = &subpass;
557
+ rpInfo.dependencyCount = 1;
558
+ rpInfo.pDependencies = &dependency;
559
+ VK_CHECK(vkCreateRenderPass(device_, &rpInfo, nullptr, &renderPass_));
560
+ }
561
+
562
+ void VulkanBackend::createDepthResources() {
563
+ if (depthImageView_) vkDestroyImageView(device_, depthImageView_, nullptr);
564
+ if (depthImage_) vkDestroyImage(device_, depthImage_, nullptr);
565
+ if (depthMemory_) vkFreeMemory(device_, depthMemory_, nullptr);
566
+
567
+ VkImageCreateInfo imgInfo{};
568
+ imgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
569
+ imgInfo.imageType = VK_IMAGE_TYPE_2D;
570
+ imgInfo.format = depthFormat_;
571
+ imgInfo.extent.width = swapchainExtent_.width;
572
+ imgInfo.extent.height = swapchainExtent_.height;
573
+ imgInfo.extent.depth = 1;
574
+ imgInfo.mipLevels = 1;
575
+ imgInfo.arrayLayers = 1;
576
+ imgInfo.samples = VK_SAMPLE_COUNT_1_BIT;
577
+ imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
578
+ imgInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
579
+ VK_CHECK(vkCreateImage(device_, &imgInfo, nullptr, &depthImage_));
580
+
581
+ VkMemoryRequirements memReqs;
582
+ vkGetImageMemoryRequirements(device_, depthImage_, &memReqs);
583
+
584
+ VkMemoryAllocateInfo allocInfo{};
585
+ allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
586
+ allocInfo.allocationSize = memReqs.size;
587
+ allocInfo.memoryTypeIndex = findMemoryType(memReqs.memoryTypeBits,
588
+ VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
589
+ VK_CHECK(vkAllocateMemory(device_, &allocInfo, nullptr, &depthMemory_));
590
+ VK_CHECK(vkBindImageMemory(device_, depthImage_, depthMemory_, 0));
591
+
592
+ VkImageViewCreateInfo viewInfo{};
593
+ viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
594
+ viewInfo.image = depthImage_;
595
+ viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
596
+ viewInfo.format = depthFormat_;
597
+ viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
598
+ viewInfo.subresourceRange.baseMipLevel = 0;
599
+ viewInfo.subresourceRange.levelCount = 1;
600
+ viewInfo.subresourceRange.baseArrayLayer = 0;
601
+ viewInfo.subresourceRange.layerCount = 1;
602
+ VK_CHECK(vkCreateImageView(device_, &viewInfo, nullptr, &depthImageView_));
603
+ }
604
+
605
+ void VulkanBackend::createFramebuffers() {
606
+ framebuffers_.resize(swapchainImageViews_.size());
607
+ for (size_t i = 0; i < swapchainImageViews_.size(); ++i) {
608
+ std::array<VkImageView, 2> attachments = {swapchainImageViews_[i], depthImageView_};
609
+
610
+ VkFramebufferCreateInfo fbInfo{};
611
+ fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
612
+ fbInfo.renderPass = renderPass_;
613
+ fbInfo.attachmentCount = static_cast<uint32_t>(attachments.size());
614
+ fbInfo.pAttachments = attachments.data();
615
+ fbInfo.width = swapchainExtent_.width;
616
+ fbInfo.height = swapchainExtent_.height;
617
+ fbInfo.layers = 1;
618
+ VK_CHECK(vkCreateFramebuffer(device_, &fbInfo, nullptr, &framebuffers_[i]));
619
+ }
620
+ }
621
+
622
+ void VulkanBackend::createCommandPool() {
623
+ VkCommandPoolCreateInfo poolInfo{};
624
+ poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
625
+ // RESET: per-buffer reset for frame command buffers.
626
+ // TRANSIENT: hint allocator to prefer fast short-lived allocations (benefits
627
+ // the one-time staging uploads that share this pool).
628
+ poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT |
629
+ VK_COMMAND_POOL_CREATE_TRANSIENT_BIT;
630
+ poolInfo.queueFamilyIndex = graphicsFamily_;
631
+ VK_CHECK(vkCreateCommandPool(device_, &poolInfo, nullptr, &commandPool_));
632
+ }
633
+
634
+ void VulkanBackend::createCommandBuffers() {
635
+ commandBuffers_.resize(kMaxFramesInFlight);
636
+ VkCommandBufferAllocateInfo allocInfo{};
637
+ allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
638
+ allocInfo.commandPool = commandPool_;
639
+ allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
640
+ allocInfo.commandBufferCount = static_cast<uint32_t>(commandBuffers_.size());
641
+ VK_CHECK(vkAllocateCommandBuffers(device_, &allocInfo, commandBuffers_.data()));
642
+ }
643
+
644
+ void VulkanBackend::createSyncObjects() {
645
+ imageAvailableSemaphores_.resize(kMaxFramesInFlight);
646
+ renderFinishedSemaphores_.resize(kMaxFramesInFlight);
647
+ inFlightFences_.resize(kMaxFramesInFlight);
648
+
649
+ VkSemaphoreCreateInfo semInfo{};
650
+ semInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
651
+
652
+ VkFenceCreateInfo fenceInfo{};
653
+ fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
654
+ fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
655
+
656
+ for (int i = 0; i < kMaxFramesInFlight; ++i) {
657
+ VK_CHECK(vkCreateSemaphore(device_, &semInfo, nullptr, &imageAvailableSemaphores_[i]));
658
+ VK_CHECK(vkCreateSemaphore(device_, &semInfo, nullptr, &renderFinishedSemaphores_[i]));
659
+ VK_CHECK(vkCreateFence(device_, &fenceInfo, nullptr, &inFlightFences_[i]));
660
+ }
661
+ }
662
+
663
+ // ── Descriptor Layouts + Pipeline Layouts ──────────────────────────────────────
664
+
665
+ void VulkanBackend::createDescriptorSetLayout() {
666
+ // Terrain set 0: UBO at binding 0 (fragment only — contains just cameraEcef).
667
+ // Vertex MVP is now in push constants; vertex stage no longer needs the UBO.
668
+ VkDescriptorSetLayoutBinding uboBinding{};
669
+ uboBinding.binding = 0;
670
+ uboBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
671
+ uboBinding.descriptorCount = 1;
672
+ uboBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
673
+
674
+ VkDescriptorSetLayoutCreateInfo uboLayoutInfo{};
675
+ uboLayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
676
+ uboLayoutInfo.bindingCount = 1;
677
+ uboLayoutInfo.pBindings = &uboBinding;
678
+ VK_CHECK(vkCreateDescriptorSetLayout(device_, &uboLayoutInfo, nullptr, &terrainDescSetLayout_));
679
+
680
+ // Terrain set 1: combined image sampler at binding 0 (fragment only)
681
+ VkDescriptorSetLayoutBinding samplerBinding{};
682
+ samplerBinding.binding = 0;
683
+ samplerBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
684
+ samplerBinding.descriptorCount = 1;
685
+ samplerBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
686
+
687
+ VkDescriptorSetLayoutCreateInfo texLayoutInfo{};
688
+ texLayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
689
+ texLayoutInfo.bindingCount = 1;
690
+ texLayoutInfo.pBindings = &samplerBinding;
691
+ VK_CHECK(vkCreateDescriptorSetLayout(device_, &texLayoutInfo, nullptr, &terrainTexLayout_));
692
+
693
+ // Sky: binding 0 = UBO only
694
+ VkDescriptorSetLayoutBinding skyUboBinding{};
695
+ skyUboBinding.binding = 0;
696
+ skyUboBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
697
+ skyUboBinding.descriptorCount = 1;
698
+ skyUboBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
699
+
700
+ VkDescriptorSetLayoutCreateInfo skyLayoutInfo{};
701
+ skyLayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
702
+ skyLayoutInfo.bindingCount = 1;
703
+ skyLayoutInfo.pBindings = &skyUboBinding;
704
+ VK_CHECK(vkCreateDescriptorSetLayout(device_, &skyLayoutInfo, nullptr, &skyDescSetLayout_));
705
+ }
706
+
707
+ void VulkanBackend::createPipelineLayout() {
708
+ // Two push constant ranges covering the 128-byte block (the Vulkan spec
709
+ // minimum guarantee): vertex reads bytes 0-63 (RTC MVP matrix), fragment
710
+ // reads bytes 64-127 (overlay + water mask + RTC centre params).
711
+ VkPushConstantRange pushRanges[2]{};
712
+ pushRanges[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
713
+ pushRanges[0].offset = 0;
714
+ pushRanges[0].size = sizeof(TerrainVertexPC); // 64 bytes
715
+
716
+ pushRanges[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
717
+ pushRanges[1].offset = sizeof(TerrainVertexPC); // 64
718
+ pushRanges[1].size = sizeof(TerrainFragmentPC); // 64 bytes
719
+
720
+ VkDescriptorSetLayout terrainSetLayouts[] = {
721
+ terrainDescSetLayout_, // set 0: UBO (cameraEcef, fragment only)
722
+ terrainTexLayout_, // set 1: imagery overlay
723
+ terrainTexLayout_, // set 2: water mask (same layout, different pool)
724
+ };
725
+
726
+ VkPipelineLayoutCreateInfo terrainLayoutInfo{};
727
+ terrainLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
728
+ terrainLayoutInfo.setLayoutCount = 3;
729
+ terrainLayoutInfo.pSetLayouts = terrainSetLayouts;
730
+ terrainLayoutInfo.pushConstantRangeCount = 2;
731
+ terrainLayoutInfo.pPushConstantRanges = pushRanges;
732
+ VK_CHECK(vkCreatePipelineLayout(device_, &terrainLayoutInfo, nullptr, &terrainPipelineLayout_));
733
+
734
+ // Sky: no push constants
735
+ VkPipelineLayoutCreateInfo skyLayoutInfo{};
736
+ skyLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
737
+ skyLayoutInfo.setLayoutCount = 1;
738
+ skyLayoutInfo.pSetLayouts = &skyDescSetLayout_;
739
+ VK_CHECK(vkCreatePipelineLayout(device_, &skyLayoutInfo, nullptr, &skyPipelineLayout_));
740
+ }
741
+
742
+ void VulkanBackend::createDescriptorPool() {
743
+ // UBO sets: kMaxFramesInFlight terrain + 1 sky
744
+ // Sampler sets: 1 fallback + up to kMaxRasterTextures raster overlay textures
745
+ std::array<VkDescriptorPoolSize, 2> poolSizes{};
746
+ poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
747
+ poolSizes[0].descriptorCount = static_cast<uint32_t>(kMaxFramesInFlight + 1);
748
+ poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
749
+ poolSizes[1].descriptorCount = static_cast<uint32_t>(kMaxRasterTextures + 1);
750
+
751
+ VkDescriptorPoolCreateInfo poolInfo{};
752
+ poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
753
+ poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
754
+ poolInfo.maxSets = static_cast<uint32_t>(kMaxFramesInFlight + 1 + kMaxRasterTextures + 1);
755
+ poolInfo.poolSizeCount = static_cast<uint32_t>(poolSizes.size());
756
+ poolInfo.pPoolSizes = poolSizes.data();
757
+ VK_CHECK(vkCreateDescriptorPool(device_, &poolInfo, nullptr, &descriptorPool_));
758
+ }
759
+
760
+ void VulkanBackend::createWaterMaskPool() {
761
+ // Separate pool for water mask descriptor sets (set 2).
762
+ // One per tile (up to kMaxRasterTextures) + 1 for the fallback.
763
+ VkDescriptorPoolSize poolSize{};
764
+ poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
765
+ poolSize.descriptorCount = static_cast<uint32_t>(kMaxRasterTextures + 1);
766
+
767
+ VkDescriptorPoolCreateInfo poolInfo{};
768
+ poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
769
+ poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
770
+ poolInfo.maxSets = static_cast<uint32_t>(kMaxRasterTextures + 1);
771
+ poolInfo.poolSizeCount = 1;
772
+ poolInfo.pPoolSizes = &poolSize;
773
+ VK_CHECK(vkCreateDescriptorPool(device_, &poolInfo, nullptr, &waterMaskPool_));
774
+ }
775
+
776
+ void VulkanBackend::createWaterMaskFallback() {
777
+ // 1×1 all-zero (land) fallback for tiles that carry no water mask texture.
778
+ // fwidth() on a uniform constant returns 0, so the coastline formula yields
779
+ // coastA = 0 and no outline is drawn — no special-casing needed in the shader.
780
+ const uint8_t zero[4] = {0, 0, 0, 255};
781
+
782
+ VkImageCreateInfo imgInfo{};
783
+ imgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
784
+ imgInfo.imageType = VK_IMAGE_TYPE_2D;
785
+ imgInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
786
+ imgInfo.extent = {1, 1, 1};
787
+ imgInfo.mipLevels = 1;
788
+ imgInfo.arrayLayers = 1;
789
+ imgInfo.samples = VK_SAMPLE_COUNT_1_BIT;
790
+ imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
791
+ imgInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
792
+ imgInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
793
+ VK_CHECK(vkCreateImage(device_, &imgInfo, nullptr, &fallbackWaterMaskTex_.image));
794
+
795
+ VkMemoryRequirements memReqs;
796
+ vkGetImageMemoryRequirements(device_, fallbackWaterMaskTex_.image, &memReqs);
797
+
798
+ VkMemoryAllocateInfo allocInfo{};
799
+ allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
800
+ allocInfo.allocationSize = memReqs.size;
801
+ allocInfo.memoryTypeIndex = findMemoryType(memReqs.memoryTypeBits,
802
+ VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
803
+ VK_CHECK(vkAllocateMemory(device_, &allocInfo, nullptr, &fallbackWaterMaskTex_.memory));
804
+ VK_CHECK(vkBindImageMemory(device_, fallbackWaterMaskTex_.image, fallbackWaterMaskTex_.memory, 0));
805
+
806
+ VkBuffer staging;
807
+ VkDeviceMemory stagingMem;
808
+ createBuffer(4, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
809
+ VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
810
+ staging, stagingMem);
811
+ void* mapped;
812
+ vkMapMemory(device_, stagingMem, 0, 4, 0, &mapped);
813
+ memcpy(mapped, zero, 4);
814
+ vkUnmapMemory(device_, stagingMem);
815
+ uploadToImage(staging, fallbackWaterMaskTex_.image, 1, 1);
816
+ vkDestroyBuffer(device_, staging, nullptr);
817
+ vkFreeMemory(device_, stagingMem, nullptr);
818
+
819
+ VkImageViewCreateInfo viewInfo{};
820
+ viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
821
+ viewInfo.image = fallbackWaterMaskTex_.image;
822
+ viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
823
+ viewInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
824
+ viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
825
+ VK_CHECK(vkCreateImageView(device_, &viewInfo, nullptr, &fallbackWaterMaskTex_.imageView));
826
+
827
+ VkSamplerCreateInfo samplerInfo{};
828
+ samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
829
+ samplerInfo.magFilter = VK_FILTER_LINEAR;
830
+ samplerInfo.minFilter = VK_FILTER_LINEAR;
831
+ samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
832
+ samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
833
+ samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
834
+ VK_CHECK(vkCreateSampler(device_, &samplerInfo, nullptr, &fallbackWaterMaskTex_.sampler));
835
+
836
+ // Allocate a descriptor set from the water mask pool.
837
+ VkDescriptorSetAllocateInfo dsAlloc{};
838
+ dsAlloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
839
+ dsAlloc.descriptorPool = waterMaskPool_;
840
+ dsAlloc.descriptorSetCount = 1;
841
+ dsAlloc.pSetLayouts = &terrainTexLayout_;
842
+ VK_CHECK(vkAllocateDescriptorSets(device_, &dsAlloc, &fallbackWaterMaskDescSet_));
843
+
844
+ VkDescriptorImageInfo dsImgInfo{};
845
+ dsImgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
846
+ dsImgInfo.imageView = fallbackWaterMaskTex_.imageView;
847
+ dsImgInfo.sampler = fallbackWaterMaskTex_.sampler;
848
+
849
+ VkWriteDescriptorSet texWrite{};
850
+ texWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
851
+ texWrite.dstSet = fallbackWaterMaskDescSet_;
852
+ texWrite.dstBinding = 0;
853
+ texWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
854
+ texWrite.descriptorCount = 1;
855
+ texWrite.pImageInfo = &dsImgInfo;
856
+ vkUpdateDescriptorSets(device_, 1, &texWrite, 0, nullptr);
857
+ }
858
+
859
+ // ── Shader compilation ─────────────────────────────────────────────────────────
860
+
861
+ static VkShaderModule createShaderModule(VkDevice device, const uint32_t* code, uint32_t codeSize) {
862
+ VkShaderModuleCreateInfo createInfo{};
863
+ createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
864
+ createInfo.codeSize = codeSize;
865
+ createInfo.pCode = code;
866
+
867
+ VkShaderModule module;
868
+ VK_CHECK(vkCreateShaderModule(device, &createInfo, nullptr, &module));
869
+ return module;
870
+ }
871
+
872
+ // ── Pipeline creation ──────────────────────────────────────────────────────────
873
+
874
+ void VulkanBackend::createSkyPipeline() {
875
+ VkShaderModule vertModule = createShaderModule(device_, spv_sky_vert, spv_sky_vert_size);
876
+ VkShaderModule fragModule = createShaderModule(device_, spv_sky_frag, spv_sky_frag_size);
877
+
878
+ VkPipelineShaderStageCreateInfo stages[2]{};
879
+ stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
880
+ stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
881
+ stages[0].module = vertModule;
882
+ stages[0].pName = "main";
883
+ stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
884
+ stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
885
+ stages[1].module = fragModule;
886
+ stages[1].pName = "main";
887
+
888
+ // No vertex input (fullscreen triangle from gl_VertexIndex)
889
+ VkPipelineVertexInputStateCreateInfo vertexInput{};
890
+ vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
891
+
892
+ VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
893
+ inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
894
+ inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
895
+
896
+ VkPipelineViewportStateCreateInfo viewportState{};
897
+ viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
898
+ viewportState.viewportCount = 1;
899
+ viewportState.scissorCount = 1;
900
+
901
+ VkPipelineRasterizationStateCreateInfo rasterizer{};
902
+ rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
903
+ rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
904
+ rasterizer.lineWidth = 1.0f;
905
+ rasterizer.cullMode = VK_CULL_MODE_NONE;
906
+ rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
907
+
908
+ VkPipelineMultisampleStateCreateInfo multisampling{};
909
+ multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
910
+ multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
911
+
912
+ VkPipelineDepthStencilStateCreateInfo depthStencil{};
913
+ depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
914
+ depthStencil.depthTestEnable = VK_FALSE;
915
+ depthStencil.depthWriteEnable = VK_FALSE;
916
+
917
+ VkPipelineColorBlendAttachmentState colorBlendAttach{};
918
+ colorBlendAttach.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
919
+ VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
920
+
921
+ VkPipelineColorBlendStateCreateInfo colorBlend{};
922
+ colorBlend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
923
+ colorBlend.attachmentCount = 1;
924
+ colorBlend.pAttachments = &colorBlendAttach;
925
+
926
+ VkDynamicState dynStates[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
927
+ VkPipelineDynamicStateCreateInfo dynamicState{};
928
+ dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
929
+ dynamicState.dynamicStateCount = 2;
930
+ dynamicState.pDynamicStates = dynStates;
931
+
932
+ VkGraphicsPipelineCreateInfo pipelineInfo{};
933
+ pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
934
+ pipelineInfo.stageCount = 2;
935
+ pipelineInfo.pStages = stages;
936
+ pipelineInfo.pVertexInputState = &vertexInput;
937
+ pipelineInfo.pInputAssemblyState = &inputAssembly;
938
+ pipelineInfo.pViewportState = &viewportState;
939
+ pipelineInfo.pRasterizationState = &rasterizer;
940
+ pipelineInfo.pMultisampleState = &multisampling;
941
+ pipelineInfo.pDepthStencilState = &depthStencil;
942
+ pipelineInfo.pColorBlendState = &colorBlend;
943
+ pipelineInfo.pDynamicState = &dynamicState;
944
+ pipelineInfo.layout = skyPipelineLayout_;
945
+ pipelineInfo.renderPass = renderPass_;
946
+ pipelineInfo.subpass = 0;
947
+ VK_CHECK(vkCreateGraphicsPipelines(device_, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &skyPipeline_));
948
+
949
+ vkDestroyShaderModule(device_, vertModule, nullptr);
950
+ vkDestroyShaderModule(device_, fragModule, nullptr);
951
+ }
952
+
953
+ void VulkanBackend::createTerrainPipeline() {
954
+ VkShaderModule vertModule = createShaderModule(device_, spv_terrain_vert, spv_terrain_vert_size);
955
+ VkShaderModule fragModule = createShaderModule(device_, spv_terrain_frag, spv_terrain_frag_size);
956
+
957
+ VkPipelineShaderStageCreateInfo stages[2]{};
958
+ stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
959
+ stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
960
+ stages[0].module = vertModule;
961
+ stages[0].pName = "main";
962
+ stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
963
+ stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
964
+ stages[1].module = fragModule;
965
+ stages[1].pName = "main";
966
+
967
+ // Vertex bindings:
968
+ // binding 0 — position (float3, tile-local)
969
+ // binding 1 — UV (float2)
970
+ // binding 2 — altitude (float1, ellipsoid height metres, double-precision source)
971
+ VkVertexInputBindingDescription bindings[3]{};
972
+ bindings[0].binding = 0;
973
+ bindings[0].stride = 3 * sizeof(float);
974
+ bindings[0].inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
975
+ bindings[1].binding = 1;
976
+ bindings[1].stride = 2 * sizeof(float);
977
+ bindings[1].inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
978
+ bindings[2].binding = 2;
979
+ bindings[2].stride = sizeof(float);
980
+ bindings[2].inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
981
+
982
+ VkVertexInputAttributeDescription attrs[3]{};
983
+ attrs[0].binding = 0;
984
+ attrs[0].location = 0;
985
+ attrs[0].format = VK_FORMAT_R32G32B32_SFLOAT;
986
+ attrs[0].offset = 0;
987
+ attrs[1].binding = 1;
988
+ attrs[1].location = 1;
989
+ attrs[1].format = VK_FORMAT_R32G32_SFLOAT;
990
+ attrs[1].offset = 0;
991
+ attrs[2].binding = 2;
992
+ attrs[2].location = 2;
993
+ attrs[2].format = VK_FORMAT_R32_SFLOAT;
994
+ attrs[2].offset = 0;
995
+
996
+ VkPipelineVertexInputStateCreateInfo vertexInput{};
997
+ vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
998
+ vertexInput.vertexBindingDescriptionCount = 3;
999
+ vertexInput.pVertexBindingDescriptions = bindings;
1000
+ vertexInput.vertexAttributeDescriptionCount = 3;
1001
+ vertexInput.pVertexAttributeDescriptions = attrs;
1002
+
1003
+ VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
1004
+ inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
1005
+ inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
1006
+
1007
+ VkPipelineViewportStateCreateInfo viewportState{};
1008
+ viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
1009
+ viewportState.viewportCount = 1;
1010
+ viewportState.scissorCount = 1;
1011
+
1012
+ VkPipelineRasterizationStateCreateInfo rasterizer{};
1013
+ rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
1014
+ rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
1015
+ rasterizer.lineWidth = 1.0f;
1016
+ rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
1017
+ rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
1018
+
1019
+ VkPipelineMultisampleStateCreateInfo multisampling{};
1020
+ multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
1021
+ multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
1022
+
1023
+ // Reversed-Z: depth clear = 0.0 (far), compare = GREATER (closer wins)
1024
+ VkPipelineDepthStencilStateCreateInfo depthStencil{};
1025
+ depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
1026
+ depthStencil.depthTestEnable = VK_TRUE;
1027
+ depthStencil.depthWriteEnable = VK_TRUE;
1028
+ depthStencil.depthCompareOp = VK_COMPARE_OP_GREATER;
1029
+
1030
+ VkPipelineColorBlendAttachmentState colorBlendAttach{};
1031
+ colorBlendAttach.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
1032
+ VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
1033
+
1034
+ VkPipelineColorBlendStateCreateInfo colorBlend{};
1035
+ colorBlend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
1036
+ colorBlend.attachmentCount = 1;
1037
+ colorBlend.pAttachments = &colorBlendAttach;
1038
+
1039
+ VkDynamicState dynStates[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
1040
+ VkPipelineDynamicStateCreateInfo dynamicState{};
1041
+ dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
1042
+ dynamicState.dynamicStateCount = 2;
1043
+ dynamicState.pDynamicStates = dynStates;
1044
+
1045
+ VkGraphicsPipelineCreateInfo pipelineInfo{};
1046
+ pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
1047
+ pipelineInfo.stageCount = 2;
1048
+ pipelineInfo.pStages = stages;
1049
+ pipelineInfo.pVertexInputState = &vertexInput;
1050
+ pipelineInfo.pInputAssemblyState = &inputAssembly;
1051
+ pipelineInfo.pViewportState = &viewportState;
1052
+ pipelineInfo.pRasterizationState = &rasterizer;
1053
+ pipelineInfo.pMultisampleState = &multisampling;
1054
+ pipelineInfo.pDepthStencilState = &depthStencil;
1055
+ pipelineInfo.pColorBlendState = &colorBlend;
1056
+ pipelineInfo.pDynamicState = &dynamicState;
1057
+ pipelineInfo.layout = terrainPipelineLayout_;
1058
+ pipelineInfo.renderPass = renderPass_;
1059
+ pipelineInfo.subpass = 0;
1060
+ VK_CHECK(vkCreateGraphicsPipelines(device_, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &terrainPipeline_));
1061
+
1062
+ vkDestroyShaderModule(device_, vertModule, nullptr);
1063
+ vkDestroyShaderModule(device_, fragModule, nullptr);
1064
+ }
1065
+
1066
+ // ── Helpers ────────────────────────────────────────────────────────────────────
1067
+
1068
+ uint32_t VulkanBackend::findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags props) {
1069
+ VkPhysicalDeviceMemoryProperties memProps;
1070
+ vkGetPhysicalDeviceMemoryProperties(physicalDevice_, &memProps);
1071
+ for (uint32_t i = 0; i < memProps.memoryTypeCount; ++i) {
1072
+ if ((typeFilter & (1 << i)) &&
1073
+ (memProps.memoryTypes[i].propertyFlags & props) == props) {
1074
+ return i;
1075
+ }
1076
+ }
1077
+ LOGE("Failed to find suitable memory type");
1078
+ return 0;
1079
+ }
1080
+
1081
+ void VulkanBackend::createBuffer(VkDeviceSize size, VkBufferUsageFlags usage,
1082
+ VkMemoryPropertyFlags props,
1083
+ VkBuffer& buffer, VkDeviceMemory& memory) {
1084
+ VkBufferCreateInfo bufInfo{};
1085
+ bufInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
1086
+ bufInfo.size = size;
1087
+ bufInfo.usage = usage;
1088
+ bufInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
1089
+ VK_CHECK(vkCreateBuffer(device_, &bufInfo, nullptr, &buffer));
1090
+
1091
+ VkMemoryRequirements memReqs;
1092
+ vkGetBufferMemoryRequirements(device_, buffer, &memReqs);
1093
+
1094
+ VkMemoryAllocateInfo allocInfo{};
1095
+ allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
1096
+ allocInfo.allocationSize = memReqs.size;
1097
+ allocInfo.memoryTypeIndex = findMemoryType(memReqs.memoryTypeBits, props);
1098
+ VK_CHECK(vkAllocateMemory(device_, &allocInfo, nullptr, &memory));
1099
+ VK_CHECK(vkBindBufferMemory(device_, buffer, memory, 0));
1100
+ }
1101
+
1102
+ void VulkanBackend::ensureBuffer(VkBuffer& buffer, VkDeviceMemory& memory,
1103
+ size_t& capacity, size_t needed,
1104
+ const void* data, VkBufferUsageFlags usage) {
1105
+ if (needed == 0) return;
1106
+ if (needed > capacity) {
1107
+ if (buffer) vkDestroyBuffer(device_, buffer, nullptr);
1108
+ if (memory) vkFreeMemory(device_, memory, nullptr);
1109
+ size_t newCap = std::max(needed, capacity > 0 ? capacity * 2 : needed);
1110
+ newCap = (newCap + 4095UL) & ~4095UL;
1111
+ createBuffer(static_cast<VkDeviceSize>(newCap), usage,
1112
+ VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
1113
+ buffer, memory);
1114
+ capacity = newCap;
1115
+ }
1116
+ void* mapped;
1117
+ vkMapMemory(device_, memory, 0, needed, 0, &mapped);
1118
+ memcpy(mapped, data, needed);
1119
+ vkUnmapMemory(device_, memory);
1120
+ }
1121
+
1122
+ VkCommandBuffer VulkanBackend::beginSingleTimeCommands() {
1123
+ VkCommandBufferAllocateInfo allocInfo{};
1124
+ allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
1125
+ allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
1126
+ allocInfo.commandPool = commandPool_;
1127
+ allocInfo.commandBufferCount = 1;
1128
+
1129
+ VkCommandBuffer cmd;
1130
+ vkAllocateCommandBuffers(device_, &allocInfo, &cmd);
1131
+
1132
+ VkCommandBufferBeginInfo beginInfo{};
1133
+ beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
1134
+ beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
1135
+ vkBeginCommandBuffer(cmd, &beginInfo);
1136
+ return cmd;
1137
+ }
1138
+
1139
+ void VulkanBackend::endSingleTimeCommands(VkCommandBuffer cmd) {
1140
+ vkEndCommandBuffer(cmd);
1141
+
1142
+ VkSubmitInfo submitInfo{};
1143
+ submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
1144
+ submitInfo.commandBufferCount = 1;
1145
+ submitInfo.pCommandBuffers = &cmd;
1146
+
1147
+ vkQueueSubmit(graphicsQueue_, 1, &submitInfo, VK_NULL_HANDLE);
1148
+ vkQueueWaitIdle(graphicsQueue_);
1149
+ vkFreeCommandBuffers(device_, commandPool_, 1, &cmd);
1150
+ }
1151
+
1152
+ void VulkanBackend::uploadToImage(VkBuffer staging, VkImage image,
1153
+ uint32_t width, uint32_t height) {
1154
+ // All three steps (UNDEFINED→TRANSFER_DST, copy, TRANSFER_DST→SHADER_READ)
1155
+ // are recorded in a single command buffer, so only one vkQueueWaitIdle is needed.
1156
+ VkCommandBuffer cmd = beginSingleTimeCommands();
1157
+
1158
+ const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
1159
+
1160
+ VkImageMemoryBarrier toTransfer{};
1161
+ toTransfer.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
1162
+ toTransfer.srcAccessMask = 0;
1163
+ toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
1164
+ toTransfer.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
1165
+ toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
1166
+ toTransfer.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
1167
+ toTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
1168
+ toTransfer.image = image;
1169
+ toTransfer.subresourceRange = range;
1170
+ vkCmdPipelineBarrier(cmd,
1171
+ VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
1172
+ 0, 0, nullptr, 0, nullptr, 1, &toTransfer);
1173
+
1174
+ VkBufferImageCopy region{};
1175
+ region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
1176
+ region.imageExtent = {width, height, 1};
1177
+ vkCmdCopyBufferToImage(cmd, staging, image,
1178
+ VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
1179
+
1180
+ VkImageMemoryBarrier toShader{};
1181
+ toShader.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
1182
+ toShader.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
1183
+ toShader.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
1184
+ toShader.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
1185
+ toShader.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
1186
+ toShader.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
1187
+ toShader.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
1188
+ toShader.image = image;
1189
+ toShader.subresourceRange = range;
1190
+ vkCmdPipelineBarrier(cmd,
1191
+ VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
1192
+ 0, 0, nullptr, 0, nullptr, 1, &toShader);
1193
+
1194
+ endSingleTimeCommands(cmd);
1195
+ }
1196
+
1197
+ // ── Fallback texture (1x1 white) ───────────────────────────────────────────────
1198
+
1199
+ void VulkanBackend::createFallbackTexture() {
1200
+ const uint8_t white[4] = {255, 255, 255, 255};
1201
+
1202
+ VkImageCreateInfo imgInfo{};
1203
+ imgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
1204
+ imgInfo.imageType = VK_IMAGE_TYPE_2D;
1205
+ imgInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
1206
+ imgInfo.extent = {1, 1, 1};
1207
+ imgInfo.mipLevels = 1;
1208
+ imgInfo.arrayLayers = 1;
1209
+ imgInfo.samples = VK_SAMPLE_COUNT_1_BIT;
1210
+ imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
1211
+ imgInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
1212
+ imgInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
1213
+ VK_CHECK(vkCreateImage(device_, &imgInfo, nullptr, &fallbackTexture_.image));
1214
+
1215
+ VkMemoryRequirements memReqs;
1216
+ vkGetImageMemoryRequirements(device_, fallbackTexture_.image, &memReqs);
1217
+
1218
+ VkMemoryAllocateInfo allocInfo{};
1219
+ allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
1220
+ allocInfo.allocationSize = memReqs.size;
1221
+ allocInfo.memoryTypeIndex = findMemoryType(memReqs.memoryTypeBits,
1222
+ VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
1223
+ VK_CHECK(vkAllocateMemory(device_, &allocInfo, nullptr, &fallbackTexture_.memory));
1224
+ VK_CHECK(vkBindImageMemory(device_, fallbackTexture_.image, fallbackTexture_.memory, 0));
1225
+
1226
+ VkBuffer staging;
1227
+ VkDeviceMemory stagingMem;
1228
+ createBuffer(4, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
1229
+ VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
1230
+ staging, stagingMem);
1231
+
1232
+ void* mapped;
1233
+ vkMapMemory(device_, stagingMem, 0, 4, 0, &mapped);
1234
+ memcpy(mapped, white, 4);
1235
+ vkUnmapMemory(device_, stagingMem);
1236
+
1237
+ uploadToImage(staging, fallbackTexture_.image, 1, 1);
1238
+
1239
+ vkDestroyBuffer(device_, staging, nullptr);
1240
+ vkFreeMemory(device_, stagingMem, nullptr);
1241
+
1242
+ VkImageViewCreateInfo viewInfo{};
1243
+ viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
1244
+ viewInfo.image = fallbackTexture_.image;
1245
+ viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
1246
+ viewInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
1247
+ viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
1248
+ VK_CHECK(vkCreateImageView(device_, &viewInfo, nullptr, &fallbackTexture_.imageView));
1249
+
1250
+ VkSamplerCreateInfo samplerInfo{};
1251
+ samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
1252
+ samplerInfo.magFilter = VK_FILTER_LINEAR;
1253
+ samplerInfo.minFilter = VK_FILTER_LINEAR;
1254
+ samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
1255
+ samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
1256
+ samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
1257
+ VK_CHECK(vkCreateSampler(device_, &samplerInfo, nullptr, &fallbackTexture_.sampler));
1258
+ }
1259
+
1260
+ // ── Raster texture creation/deletion ───────────────────────────────────────────
1261
+
1262
+ void* VulkanBackend::createRasterTexture(const uint8_t* pixels, int32_t width, int32_t height) {
1263
+ if (!device_ || !pixels || width <= 0 || height <= 0) return nullptr;
1264
+
1265
+ auto* tex = new VulkanTexture();
1266
+ VkDeviceSize imageSize = static_cast<VkDeviceSize>(width) * height * 4;
1267
+
1268
+ VkBuffer staging;
1269
+ VkDeviceMemory stagingMem;
1270
+ createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
1271
+ VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
1272
+ staging, stagingMem);
1273
+
1274
+ void* mapped;
1275
+ vkMapMemory(device_, stagingMem, 0, imageSize, 0, &mapped);
1276
+ memcpy(mapped, pixels, static_cast<size_t>(imageSize));
1277
+ vkUnmapMemory(device_, stagingMem);
1278
+
1279
+ VkImageCreateInfo imgInfo{};
1280
+ imgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
1281
+ imgInfo.imageType = VK_IMAGE_TYPE_2D;
1282
+ imgInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
1283
+ imgInfo.extent = {static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1};
1284
+ imgInfo.mipLevels = 1;
1285
+ imgInfo.arrayLayers = 1;
1286
+ imgInfo.samples = VK_SAMPLE_COUNT_1_BIT;
1287
+ imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
1288
+ imgInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
1289
+ imgInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
1290
+ VK_CHECK(vkCreateImage(device_, &imgInfo, nullptr, &tex->image));
1291
+
1292
+ VkMemoryRequirements memReqs;
1293
+ vkGetImageMemoryRequirements(device_, tex->image, &memReqs);
1294
+
1295
+ VkMemoryAllocateInfo allocInfo{};
1296
+ allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
1297
+ allocInfo.allocationSize = memReqs.size;
1298
+ allocInfo.memoryTypeIndex = findMemoryType(memReqs.memoryTypeBits,
1299
+ VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
1300
+ VK_CHECK(vkAllocateMemory(device_, &allocInfo, nullptr, &tex->memory));
1301
+ VK_CHECK(vkBindImageMemory(device_, tex->image, tex->memory, 0));
1302
+
1303
+ uploadToImage(staging, tex->image,
1304
+ static_cast<uint32_t>(width), static_cast<uint32_t>(height));
1305
+
1306
+ vkDestroyBuffer(device_, staging, nullptr);
1307
+ vkFreeMemory(device_, stagingMem, nullptr);
1308
+
1309
+ VkImageViewCreateInfo viewInfo{};
1310
+ viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
1311
+ viewInfo.image = tex->image;
1312
+ viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
1313
+ viewInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
1314
+ viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
1315
+ VK_CHECK(vkCreateImageView(device_, &viewInfo, nullptr, &tex->imageView));
1316
+
1317
+ VkSamplerCreateInfo samplerInfo{};
1318
+ samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
1319
+ samplerInfo.magFilter = VK_FILTER_LINEAR;
1320
+ samplerInfo.minFilter = VK_FILTER_LINEAR;
1321
+ samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
1322
+ samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
1323
+ samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
1324
+ VK_CHECK(vkCreateSampler(device_, &samplerInfo, nullptr, &tex->sampler));
1325
+
1326
+ // Allocate a descriptor set for this texture (set 1: texture only)
1327
+ VkDescriptorSetAllocateInfo dsAllocInfo{};
1328
+ dsAllocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
1329
+ dsAllocInfo.descriptorPool = descriptorPool_;
1330
+ dsAllocInfo.descriptorSetCount = 1;
1331
+ dsAllocInfo.pSetLayouts = &terrainTexLayout_;
1332
+ VkResult dsResult = vkAllocateDescriptorSets(device_, &dsAllocInfo, &tex->descriptorSet);
1333
+ if (dsResult != VK_SUCCESS) {
1334
+ LOGE("vkAllocateDescriptorSets failed: %d (pool exhausted?)", dsResult);
1335
+ tex->descriptorSet = VK_NULL_HANDLE;
1336
+ } else {
1337
+ VkDescriptorImageInfo dsImgInfo{};
1338
+ dsImgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
1339
+ dsImgInfo.imageView = tex->imageView;
1340
+ dsImgInfo.sampler = tex->sampler;
1341
+
1342
+ VkWriteDescriptorSet texWrite{};
1343
+ texWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
1344
+ texWrite.dstSet = tex->descriptorSet;
1345
+ texWrite.dstBinding = 0;
1346
+ texWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
1347
+ texWrite.descriptorCount = 1;
1348
+ texWrite.pImageInfo = &dsImgInfo;
1349
+ vkUpdateDescriptorSets(device_, 1, &texWrite, 0, nullptr);
1350
+ }
1351
+
1352
+ return tex;
1353
+ }
1354
+
1355
+ void VulkanBackend::freeRasterTexture(void* texPtr) {
1356
+ if (!texPtr || !device_) return;
1357
+ auto* tex = static_cast<VulkanTexture*>(texPtr);
1358
+ // Enqueue for deferred deletion — pool = descriptorPool_ (set 1 imagery pool).
1359
+ std::lock_guard<std::mutex> lk(pendingDeletesMutex_);
1360
+ pendingDeletes_.push_back({tex, totalFrameCount_, descriptorPool_});
1361
+ }
1362
+
1363
+ void* VulkanBackend::createWaterMaskTexture(const uint8_t* pixels, int32_t width, int32_t height) {
1364
+ if (!device_ || !pixels || width <= 0 || height <= 0 || !waterMaskPool_) return nullptr;
1365
+
1366
+ auto* tex = new VulkanTexture();
1367
+ VkDeviceSize imageSize = static_cast<VkDeviceSize>(width) * height * 4;
1368
+
1369
+ VkBuffer staging;
1370
+ VkDeviceMemory stagingMem;
1371
+ createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
1372
+ VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
1373
+ staging, stagingMem);
1374
+
1375
+ void* mapped;
1376
+ vkMapMemory(device_, stagingMem, 0, imageSize, 0, &mapped);
1377
+ memcpy(mapped, pixels, static_cast<size_t>(imageSize));
1378
+ vkUnmapMemory(device_, stagingMem);
1379
+
1380
+ VkImageCreateInfo imgInfo{};
1381
+ imgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
1382
+ imgInfo.imageType = VK_IMAGE_TYPE_2D;
1383
+ imgInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
1384
+ imgInfo.extent = {static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1};
1385
+ imgInfo.mipLevels = 1;
1386
+ imgInfo.arrayLayers = 1;
1387
+ imgInfo.samples = VK_SAMPLE_COUNT_1_BIT;
1388
+ imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
1389
+ imgInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
1390
+ imgInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
1391
+ VK_CHECK(vkCreateImage(device_, &imgInfo, nullptr, &tex->image));
1392
+
1393
+ VkMemoryRequirements memReqs;
1394
+ vkGetImageMemoryRequirements(device_, tex->image, &memReqs);
1395
+
1396
+ VkMemoryAllocateInfo allocInfo{};
1397
+ allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
1398
+ allocInfo.allocationSize = memReqs.size;
1399
+ allocInfo.memoryTypeIndex = findMemoryType(memReqs.memoryTypeBits,
1400
+ VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
1401
+ VK_CHECK(vkAllocateMemory(device_, &allocInfo, nullptr, &tex->memory));
1402
+ VK_CHECK(vkBindImageMemory(device_, tex->image, tex->memory, 0));
1403
+
1404
+ uploadToImage(staging, tex->image,
1405
+ static_cast<uint32_t>(width), static_cast<uint32_t>(height));
1406
+
1407
+ vkDestroyBuffer(device_, staging, nullptr);
1408
+ vkFreeMemory(device_, stagingMem, nullptr);
1409
+
1410
+ VkImageViewCreateInfo viewInfo{};
1411
+ viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
1412
+ viewInfo.image = tex->image;
1413
+ viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
1414
+ viewInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
1415
+ viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
1416
+ VK_CHECK(vkCreateImageView(device_, &viewInfo, nullptr, &tex->imageView));
1417
+
1418
+ VkSamplerCreateInfo samplerInfo{};
1419
+ samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
1420
+ samplerInfo.magFilter = VK_FILTER_LINEAR;
1421
+ samplerInfo.minFilter = VK_FILTER_LINEAR;
1422
+ samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
1423
+ samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
1424
+ samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
1425
+ VK_CHECK(vkCreateSampler(device_, &samplerInfo, nullptr, &tex->sampler));
1426
+
1427
+ // Allocate descriptor set from the water mask pool (set 2).
1428
+ VkDescriptorSetAllocateInfo dsAllocInfo{};
1429
+ dsAllocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
1430
+ dsAllocInfo.descriptorPool = waterMaskPool_;
1431
+ dsAllocInfo.descriptorSetCount = 1;
1432
+ dsAllocInfo.pSetLayouts = &terrainTexLayout_;
1433
+ VkResult dsResult = vkAllocateDescriptorSets(device_, &dsAllocInfo, &tex->descriptorSet);
1434
+ if (dsResult != VK_SUCCESS) {
1435
+ LOGE("Water mask descriptor set alloc FAILED: %d", dsResult);
1436
+ tex->descriptorSet = VK_NULL_HANDLE;
1437
+ } else {
1438
+ VkDescriptorImageInfo dsImgInfo{};
1439
+ dsImgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
1440
+ dsImgInfo.imageView = tex->imageView;
1441
+ dsImgInfo.sampler = tex->sampler;
1442
+
1443
+ VkWriteDescriptorSet texWrite{};
1444
+ texWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
1445
+ texWrite.dstSet = tex->descriptorSet;
1446
+ texWrite.dstBinding = 0;
1447
+ texWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
1448
+ texWrite.descriptorCount = 1;
1449
+ texWrite.pImageInfo = &dsImgInfo;
1450
+ vkUpdateDescriptorSets(device_, 1, &texWrite, 0, nullptr);
1451
+ }
1452
+
1453
+ return tex;
1454
+ }
1455
+
1456
+ void VulkanBackend::freeWaterMaskTexture(void* texPtr) {
1457
+ if (!texPtr || !device_) return;
1458
+ auto* tex = static_cast<VulkanTexture*>(texPtr);
1459
+ // Enqueue for deferred deletion — pool = waterMaskPool_ (set 2 pool).
1460
+ std::lock_guard<std::mutex> lk(pendingDeletesMutex_);
1461
+ pendingDeletes_.push_back({tex, totalFrameCount_, waterMaskPool_});
1462
+ }
1463
+
1464
+ void VulkanBackend::destroyTexture(VulkanTexture* tex, VkDescriptorPool pool) {
1465
+ if (!tex) return;
1466
+ if (tex->descriptorSet && pool != VK_NULL_HANDLE) {
1467
+ vkFreeDescriptorSets(device_, pool, 1, &tex->descriptorSet);
1468
+ }
1469
+ if (tex->sampler) vkDestroySampler(device_, tex->sampler, nullptr);
1470
+ if (tex->imageView) vkDestroyImageView(device_, tex->imageView, nullptr);
1471
+ if (tex->image) vkDestroyImage(device_, tex->image, nullptr);
1472
+ if (tex->memory) vkFreeMemory(device_, tex->memory, nullptr);
1473
+ delete tex;
1474
+ }
1475
+
1476
+ void VulkanBackend::flushPendingDeletes() {
1477
+ std::lock_guard<std::mutex> lk(pendingDeletesMutex_);
1478
+ // Entries are enqueued in monotonically increasing frameIndex order, so we
1479
+ // can stop as soon as the front entry is not yet safe to destroy.
1480
+ while (!pendingDeletes_.empty()) {
1481
+ const auto& front = pendingDeletes_.front();
1482
+ if (totalFrameCount_ < front.frameIndex + static_cast<uint64_t>(kMaxFramesInFlight))
1483
+ break;
1484
+ destroyTexture(front.tex, front.pool);
1485
+ pendingDeletes_.pop_front();
1486
+ }
1487
+ }
1488
+
1489
+ void VulkanBackend::setMsaaSampleCount(int /*sampleCount*/) {
1490
+ // MSAA not implemented in initial version — requires resolve attachments.
1491
+ // Accepted but ignored; can be wired up in a follow-up pass.
1492
+ }
1493
+
1494
+ // ── Swapchain recreation ───────────────────────────────────────────────────────
1495
+
1496
+ void VulkanBackend::cleanupSwapchain() {
1497
+ for (auto& fb : framebuffers_) vkDestroyFramebuffer(device_, fb, nullptr);
1498
+ framebuffers_.clear();
1499
+
1500
+ if (depthImageView_) vkDestroyImageView(device_, depthImageView_, nullptr);
1501
+ if (depthImage_) vkDestroyImage(device_, depthImage_, nullptr);
1502
+ if (depthMemory_) vkFreeMemory(device_, depthMemory_, nullptr);
1503
+ depthImageView_ = VK_NULL_HANDLE;
1504
+ depthImage_ = VK_NULL_HANDLE;
1505
+ depthMemory_ = VK_NULL_HANDLE;
1506
+
1507
+ for (auto& iv : swapchainImageViews_) vkDestroyImageView(device_, iv, nullptr);
1508
+ swapchainImageViews_.clear();
1509
+
1510
+ if (swapchain_) {
1511
+ vkDestroySwapchainKHR(device_, swapchain_, nullptr);
1512
+ swapchain_ = VK_NULL_HANDLE;
1513
+ }
1514
+ }
1515
+
1516
+ void VulkanBackend::recreateSwapchain() {
1517
+ vkDeviceWaitIdle(device_);
1518
+ cleanupSwapchain();
1519
+ createSwapchain();
1520
+ createDepthResources();
1521
+ createFramebuffers();
1522
+ needsSwapchainRecreate_ = false;
1523
+ }
1524
+
1525
+ // ── Per-frame rendering ────────────────────────────────────────────────────────
1526
+
1527
+ void VulkanBackend::beginFrame(const FrameParams& /*params*/) {
1528
+ if (device_ == VK_NULL_HANDLE) return;
1529
+
1530
+ if (needsSwapchainRecreate_) {
1531
+ recreateSwapchain();
1532
+ }
1533
+
1534
+ vkWaitForFences(device_, 1, &inFlightFences_[frameIndex_], VK_TRUE, UINT64_MAX);
1535
+
1536
+ // GPU is done with frameIndex_ — safe to destroy textures enqueued that long ago.
1537
+ ++totalFrameCount_;
1538
+ flushPendingDeletes();
1539
+
1540
+ VkResult result = vkAcquireNextImageKHR(device_, swapchain_, UINT64_MAX,
1541
+ imageAvailableSemaphores_[frameIndex_],
1542
+ VK_NULL_HANDLE, &imageIndex_);
1543
+ if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
1544
+ recreateSwapchain();
1545
+ result = vkAcquireNextImageKHR(device_, swapchain_, UINT64_MAX,
1546
+ imageAvailableSemaphores_[frameIndex_],
1547
+ VK_NULL_HANDLE, &imageIndex_);
1548
+ }
1549
+ if (result != VK_SUCCESS) {
1550
+ frameBegan_ = false;
1551
+ return;
1552
+ }
1553
+
1554
+ vkResetFences(device_, 1, &inFlightFences_[frameIndex_]);
1555
+ vkResetCommandBuffer(commandBuffers_[frameIndex_], 0);
1556
+
1557
+ VkCommandBufferBeginInfo beginInfo{};
1558
+ beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
1559
+ vkBeginCommandBuffer(commandBuffers_[frameIndex_], &beginInfo);
1560
+
1561
+ std::array<VkClearValue, 2> clearValues{};
1562
+ clearValues[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}};
1563
+ clearValues[1].depthStencil = {0.0f, 0}; // Reversed-Z: 0 = far
1564
+
1565
+ VkRenderPassBeginInfo rpBegin{};
1566
+ rpBegin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
1567
+ rpBegin.renderPass = renderPass_;
1568
+ rpBegin.framebuffer = framebuffers_[imageIndex_];
1569
+ rpBegin.renderArea.offset = {0, 0};
1570
+ rpBegin.renderArea.extent = swapchainExtent_;
1571
+ rpBegin.clearValueCount = static_cast<uint32_t>(clearValues.size());
1572
+ rpBegin.pClearValues = clearValues.data();
1573
+
1574
+ vkCmdBeginRenderPass(commandBuffers_[frameIndex_], &rpBegin, VK_SUBPASS_CONTENTS_INLINE);
1575
+
1576
+ // Negative viewport height flips Y to match the OpenGL/Metal clip-space
1577
+ // convention that the shared C++ projection matrix targets (Vulkan 1.1+).
1578
+ // The spec auto-adjusts triangle winding so VK_FRONT_FACE_COUNTER_CLOCKWISE
1579
+ // still works correctly.
1580
+ VkViewport viewport{};
1581
+ viewport.x = 0.0f;
1582
+ viewport.y = static_cast<float>(swapchainExtent_.height);
1583
+ viewport.width = static_cast<float>(swapchainExtent_.width);
1584
+ viewport.height = -static_cast<float>(swapchainExtent_.height);
1585
+ viewport.minDepth = 0.0f;
1586
+ viewport.maxDepth = 1.0f;
1587
+ vkCmdSetViewport(commandBuffers_[frameIndex_], 0, 1, &viewport);
1588
+
1589
+ VkRect2D scissor{};
1590
+ scissor.offset = {0, 0};
1591
+ scissor.extent = swapchainExtent_;
1592
+ vkCmdSetScissor(commandBuffers_[frameIndex_], 0, 1, &scissor);
1593
+
1594
+ frameBegan_ = true;
1595
+ }
1596
+
1597
+ void VulkanBackend::drawScene(const FrameResult& frame) {
1598
+ if (!frameBegan_) return;
1599
+
1600
+ VkCommandBuffer cmd = commandBuffers_[frameIndex_];
1601
+
1602
+ // ── Sky pass ──────────────────────────────────────────────────────────────
1603
+ if (skyPipeline_ && skyDescSet_) {
1604
+ SkyUBO skyU{};
1605
+ const float* iv = glm::value_ptr(frame.invVP);
1606
+ for (int i = 0; i < 16; ++i) skyU.invVP[i] = iv[i];
1607
+ skyU.cameraEcef[0] = frame.cameraEcef.x;
1608
+ skyU.cameraEcef[1] = frame.cameraEcef.y;
1609
+ skyU.cameraEcef[2] = frame.cameraEcef.z;
1610
+ float cl = sqrtf(frame.cameraEcef.x * frame.cameraEcef.x +
1611
+ frame.cameraEcef.y * frame.cameraEcef.y +
1612
+ frame.cameraEcef.z * frame.cameraEcef.z);
1613
+ if (cl > 0.0f) {
1614
+ skyU.lightDir[0] = frame.cameraEcef.x / cl;
1615
+ skyU.lightDir[1] = frame.cameraEcef.y / cl;
1616
+ skyU.lightDir[2] = frame.cameraEcef.z / cl;
1617
+ }
1618
+
1619
+ memcpy(skyUboMapped_, &skyU, sizeof(SkyUBO));
1620
+
1621
+ vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, skyPipeline_);
1622
+ vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, skyPipelineLayout_,
1623
+ 0, 1, &skyDescSet_, 0, nullptr);
1624
+ vkCmdDraw(cmd, 3, 1, 0, 0);
1625
+ }
1626
+
1627
+ // ── Terrain pass ──────────────────────────────────────────────────────────
1628
+ if (frame.draws.empty() || frame.localPositions.empty() || frame.indices.empty() ||
1629
+ frame.altitudes.empty() || !terrainPipeline_) {
1630
+ return;
1631
+ }
1632
+
1633
+ const int fi = frameIndex_;
1634
+
1635
+ const size_t vtxBytes = frame.localPositions.size() * sizeof(float);
1636
+ const size_t idxBytes = frame.indices.size() * sizeof(uint32_t);
1637
+ const size_t uvBytes = frame.uvs.size() * sizeof(float);
1638
+ const size_t altBytes = frame.altitudes.size() * sizeof(float);
1639
+
1640
+ ensureBuffer(vtxBufs_[fi], vtxMems_[fi], vtxCaps_[fi], vtxBytes,
1641
+ frame.localPositions.data(), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
1642
+ ensureBuffer(idxBufs_[fi], idxMems_[fi], idxCaps_[fi], idxBytes,
1643
+ frame.indices.data(), VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
1644
+ ensureBuffer(uvBufs_[fi], uvMems_[fi], uvCaps_[fi], uvBytes,
1645
+ frame.uvs.data(), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
1646
+ ensureBuffer(altBufs_[fi], altMems_[fi], altCaps_[fi], altBytes,
1647
+ frame.altitudes.data(), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
1648
+
1649
+ if (!vtxBufs_[fi] || !idxBufs_[fi]) return;
1650
+
1651
+ // Per-frame UBO: just cameraEcef (fragment uses it for lighting view direction).
1652
+ TerrainUBO terrU{};
1653
+ terrU.cameraEcef[0] = frame.cameraEcef.x;
1654
+ terrU.cameraEcef[1] = frame.cameraEcef.y;
1655
+ terrU.cameraEcef[2] = frame.cameraEcef.z;
1656
+ terrU.cameraEcef[3] = 0.0f;
1657
+ memcpy(terrainUboMapped_[fi], &terrU, sizeof(TerrainUBO));
1658
+
1659
+ vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, terrainPipeline_);
1660
+
1661
+ // Bind vertex buffers: binding 0 = positions, 1 = UVs, 2 = altitudes.
1662
+ VkBuffer vertexBuffers[] = {vtxBufs_[fi], uvBufs_[fi], altBufs_[fi]};
1663
+ VkDeviceSize offsets[] = {0, 0, 0};
1664
+ vkCmdBindVertexBuffers(cmd, 0, 3, vertexBuffers, offsets);
1665
+ vkCmdBindIndexBuffer(cmd, idxBufs_[fi], 0, VK_INDEX_TYPE_UINT32);
1666
+
1667
+ // Bind set 0 (UBO) once for the whole terrain pass.
1668
+ vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, terrainPipelineLayout_,
1669
+ 0, 1, &terrainDescSets_[fi], 0, nullptr);
1670
+
1671
+ // Bind set 1 (imagery) and set 2 (water mask) — start with fallbacks.
1672
+ VkDescriptorSet lastTexSet = fallbackTexDescSet_;
1673
+ VkDescriptorSet lastWaterMaskSet = fallbackWaterMaskDescSet_;
1674
+ vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, terrainPipelineLayout_,
1675
+ 1, 1, &lastTexSet, 0, nullptr);
1676
+ vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, terrainPipelineLayout_,
1677
+ 2, 1, &lastWaterMaskSet, 0, nullptr);
1678
+
1679
+ for (const auto& draw : frame.draws) {
1680
+ if (draw.indexCount == 0) continue;
1681
+
1682
+ // ── Vertex push constants: per-tile RTC MVP (bytes 0-63) ─────────────────
1683
+ TerrainVertexPC vpc{};
1684
+ const float* mp = glm::value_ptr(draw.mvpMatrix);
1685
+ for (int i = 0; i < 16; ++i) vpc.mvpMatrix[i] = mp[i];
1686
+ vkCmdPushConstants(cmd, terrainPipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT,
1687
+ 0, sizeof(TerrainVertexPC), &vpc);
1688
+
1689
+ // ── Fragment push constants: overlay + water mask + RTC (bytes 64-127) ───
1690
+ TerrainFragmentPC fpc{};
1691
+ fpc.hasOverlay = (draw.overlayTexture && draw.hasUVs) ? 1u : 0u;
1692
+ fpc.isEllipsoidFallback = draw.isEllipsoidFallback ? 1u : 0u;
1693
+ fpc.isOnlyWater = draw.isOnlyWater ? 1u : 0u;
1694
+ fpc.hasWaterMask = (draw.waterMaskTexture != nullptr) ? 1u : 0u;
1695
+ fpc.wmWest = draw.wmTileBounds.x;
1696
+ fpc.wmSouth = draw.wmTileBounds.y;
1697
+ fpc.wmEast = draw.wmTileBounds.z;
1698
+ fpc.wmNorth = draw.wmTileBounds.w;
1699
+ fpc.rtcCenterX = draw.rtcCenterEcef.x;
1700
+ fpc.rtcCenterY = draw.rtcCenterEcef.y;
1701
+ fpc.rtcCenterZ = draw.rtcCenterEcef.z;
1702
+ fpc.translationX = draw.overlayTranslation.x;
1703
+ fpc.translationY = draw.overlayTranslation.y;
1704
+ fpc.scaleX = draw.overlayScale.x;
1705
+ fpc.scaleY = draw.overlayScale.y;
1706
+ vkCmdPushConstants(cmd, terrainPipelineLayout_, VK_SHADER_STAGE_FRAGMENT_BIT,
1707
+ sizeof(TerrainVertexPC), sizeof(TerrainFragmentPC), &fpc);
1708
+
1709
+ // ── Set 1: imagery overlay ───────────────────────────────────────────────
1710
+ VkDescriptorSet texSet = fallbackTexDescSet_;
1711
+ if (draw.overlayTexture && draw.hasUVs) {
1712
+ auto* tex = static_cast<VulkanTexture*>(draw.overlayTexture);
1713
+ if (tex && tex->descriptorSet) {
1714
+ texSet = tex->descriptorSet;
1715
+ }
1716
+ }
1717
+ if (texSet != lastTexSet) {
1718
+ lastTexSet = texSet;
1719
+ vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, terrainPipelineLayout_,
1720
+ 1, 1, &lastTexSet, 0, nullptr);
1721
+ }
1722
+
1723
+ // ── Set 2: water mask ────────────────────────────────────────────────────
1724
+ VkDescriptorSet wmSet = fallbackWaterMaskDescSet_;
1725
+ if (draw.waterMaskTexture) {
1726
+ auto* wmTex = static_cast<VulkanTexture*>(draw.waterMaskTexture);
1727
+ if (wmTex && wmTex->descriptorSet) {
1728
+ wmSet = wmTex->descriptorSet;
1729
+ }
1730
+ }
1731
+ if (wmSet != lastWaterMaskSet) {
1732
+ lastWaterMaskSet = wmSet;
1733
+ vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, terrainPipelineLayout_,
1734
+ 2, 1, &lastWaterMaskSet, 0, nullptr);
1735
+ }
1736
+
1737
+ uint32_t firstIndex = draw.indexByteOffset / sizeof(uint32_t);
1738
+ vkCmdDrawIndexed(cmd, draw.indexCount, 1, firstIndex, 0, 0);
1739
+ }
1740
+ }
1741
+
1742
+ void VulkanBackend::endFrame() {
1743
+ if (!frameBegan_) return;
1744
+ frameBegan_ = false;
1745
+
1746
+ VkCommandBuffer cmd = commandBuffers_[frameIndex_];
1747
+ vkCmdEndRenderPass(cmd);
1748
+ VK_CHECK(vkEndCommandBuffer(cmd));
1749
+
1750
+ VkSemaphore waitSemaphores[] = {imageAvailableSemaphores_[frameIndex_]};
1751
+ VkSemaphore signalSemaphores[] = {renderFinishedSemaphores_[frameIndex_]};
1752
+ VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
1753
+
1754
+ VkSubmitInfo submitInfo{};
1755
+ submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
1756
+ submitInfo.waitSemaphoreCount = 1;
1757
+ submitInfo.pWaitSemaphores = waitSemaphores;
1758
+ submitInfo.pWaitDstStageMask = waitStages;
1759
+ submitInfo.commandBufferCount = 1;
1760
+ submitInfo.pCommandBuffers = &cmd;
1761
+ submitInfo.signalSemaphoreCount = 1;
1762
+ submitInfo.pSignalSemaphores = signalSemaphores;
1763
+ VK_CHECK(vkQueueSubmit(graphicsQueue_, 1, &submitInfo, inFlightFences_[frameIndex_]));
1764
+
1765
+ VkPresentInfoKHR presentInfo{};
1766
+ presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
1767
+ presentInfo.waitSemaphoreCount = 1;
1768
+ presentInfo.pWaitSemaphores = signalSemaphores;
1769
+ presentInfo.swapchainCount = 1;
1770
+ presentInfo.pSwapchains = &swapchain_;
1771
+ presentInfo.pImageIndices = &imageIndex_;
1772
+
1773
+ VkResult result = vkQueuePresentKHR(presentQueue_, &presentInfo);
1774
+ if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
1775
+ needsSwapchainRecreate_ = true;
1776
+ }
1777
+
1778
+ frameIndex_ = (frameIndex_ + 1) % kMaxFramesInFlight;
1779
+ }
1780
+
1781
+ } // namespace reactnativecesium