react-native-cesium 1.0.6 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -11
- package/ReactNativeCesium.podspec +1 -1
- package/android/CMakeLists.txt +5 -3
- package/android/src/main/cpp/CesiumBridgeAndroid.cpp +197 -227
- package/android/src/main/cpp/CesiumBridgeAndroid.h +48 -49
- package/android/src/main/cpp/cpp-adapter.cpp +2 -2
- package/android/src/main/cpp/vulkan/VulkanBackend.cpp +1010 -569
- package/android/src/main/cpp/vulkan/VulkanBackend.h +121 -12
- package/android/src/main/cpp/vulkan/shaders/sky.frag +20 -10
- package/android/src/main/java/com/margelo/nitro/reactnativecesium/CesiumBridgeJNI.kt +28 -0
- package/android/src/main/java/com/margelo/nitro/reactnativecesium/HybridCesiumView.kt +295 -58
- package/cpp/engine/CameraSmoother.cpp +99 -0
- package/cpp/engine/CameraSmoother.hpp +29 -0
- package/cpp/engine/CameraTargetState.cpp +49 -0
- package/cpp/engine/CameraTargetState.hpp +121 -0
- package/cpp/engine/CesiumEngine.cpp +206 -60
- package/cpp/engine/CesiumEngine.hpp +77 -10
- package/cpp/engine/EngineTunables.hpp +79 -0
- package/cpp/engine/GeoidConverter.cpp +36 -0
- package/cpp/engine/GeoidConverter.hpp +10 -0
- package/cpp/engine/GlobeCamera.cpp +7 -2
- package/cpp/engine/MetricsAggregator.cpp +71 -0
- package/cpp/engine/MetricsAggregator.hpp +54 -0
- package/cpp/engine/TileLifecycleManager.cpp +0 -12
- package/cpp/engine/TileLifecycleManager.hpp +0 -3
- package/cpp/third_party/egm96/EGM96.c +289 -0
- package/cpp/third_party/egm96/EGM96.h +45 -0
- package/cpp/third_party/egm96/EGM96_data.h +65372 -0
- package/ios/CesiumBridge.h +24 -5
- package/ios/CesiumBridge.mm +149 -271
- package/ios/HybridCesiumView.swift +197 -40
- package/ios/metal/MetalBackend.mm +72 -23
- package/lib/typescript/src/specs/cesium-view.nitro.d.ts +26 -0
- package/lib/typescript/src/specs/cesium-view.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JCesiumMetrics.hpp +5 -1
- package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.cpp +109 -1
- package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.hpp +24 -0
- package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.cpp +48 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/CameraState.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/CesiumMetrics.kt +32 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/HybridCesiumViewSpec.kt +72 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/Quaternion.kt +19 -0
- package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Bridge.hpp +30 -0
- package/nitrogen/generated/ios/c++/HybridCesiumViewSpecSwift.hpp +85 -1
- package/nitrogen/generated/ios/c++/views/HybridCesiumViewComponent.mm +60 -0
- package/nitrogen/generated/ios/swift/CesiumMetrics.swift +7 -2
- package/nitrogen/generated/ios/swift/HybridCesiumViewSpec.swift +12 -0
- package/nitrogen/generated/ios/swift/HybridCesiumViewSpec_cxx.swift +288 -0
- package/nitrogen/generated/shared/c++/CesiumMetrics.hpp +5 -1
- package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.cpp +24 -0
- package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.hpp +25 -1
- package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.cpp +132 -0
- package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.hpp +13 -1
- package/nitrogen/generated/shared/json/CesiumViewConfig.json +12 -0
- package/package.json +4 -4
- package/scripts/cesium/clone.mjs +1 -1
- package/src/specs/cesium-view.nitro.ts +29 -0
- package/cpp/engine/DeletionQueue.cpp +0 -28
- package/cpp/engine/DeletionQueue.hpp +0 -28
|
@@ -12,11 +12,26 @@
|
|
|
12
12
|
// - Reversed-Z infinite projection: depth clear = 0, compare = GREATER.
|
|
13
13
|
// - Sky drawn first (no depth write), terrain drawn after.
|
|
14
14
|
// - Water mask: UV computed from geographic lat/lon + tile bounds (not fragUV).
|
|
15
|
-
// - Push constants
|
|
15
|
+
// - Push constants: bytes 0-63 vertex (MVP), bytes 64-127 fragment.
|
|
16
16
|
// - Triple-buffered persistent VkBuffers guarded by VkFence.
|
|
17
|
+
//
|
|
18
|
+
// Texture upload pipeline (P0-vk-uploads):
|
|
19
|
+
// - Persistent host-visible staging ring (64 MB, mapped once at init).
|
|
20
|
+
// - createRasterTexture / createWaterMaskTexture allocate a slice + memcpy
|
|
21
|
+
// pixels in, then enqueue a PendingUpload — they DO NOT submit a command
|
|
22
|
+
// buffer or call vkQueueWaitIdle.
|
|
23
|
+
// - beginFrame submits the queued uploads in one command buffer with batched
|
|
24
|
+
// pipeline barriers, signalling the per-frame upload fence. The matching
|
|
25
|
+
// fence is waited on N=kMaxFramesInFlight frames later, at which point the
|
|
26
|
+
// staging slices are reclaimed.
|
|
27
|
+
// - Textures are guarded by a `pendingUpload` flag — drawScene falls back to
|
|
28
|
+
// the white texture for a tile whose pixels have not yet hit VRAM (one
|
|
29
|
+
// frame at most).
|
|
17
30
|
|
|
18
31
|
#include "VulkanBackend.h"
|
|
19
32
|
|
|
33
|
+
#include "engine/EngineTunables.hpp"
|
|
34
|
+
|
|
20
35
|
#include <android/native_window.h>
|
|
21
36
|
#include <android/log.h>
|
|
22
37
|
|
|
@@ -25,10 +40,12 @@
|
|
|
25
40
|
#include <algorithm>
|
|
26
41
|
#include <array>
|
|
27
42
|
#include <cassert>
|
|
43
|
+
#include <cmath>
|
|
44
|
+
#include <cstdio>
|
|
28
45
|
#include <cstring>
|
|
29
|
-
#include <
|
|
30
|
-
#include <mutex>
|
|
46
|
+
#include <fstream>
|
|
31
47
|
#include <stdexcept>
|
|
48
|
+
#include <vector>
|
|
32
49
|
|
|
33
50
|
// Pre-compiled SPIR-V headers (generated at build time by glslc + spv_to_header.cmake)
|
|
34
51
|
#include "terrain.vert.spv.h"
|
|
@@ -38,13 +55,24 @@
|
|
|
38
55
|
|
|
39
56
|
#define LOG_TAG "VulkanBackend"
|
|
40
57
|
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
|
58
|
+
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
|
|
41
59
|
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
|
42
60
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
61
|
+
// VK_CHECK throws on error during init paths — these are non-recoverable. For
|
|
62
|
+
// per-frame paths VkResult is inspected explicitly so we can degrade gracefully.
|
|
63
|
+
#define VK_CHECK(call) do { \
|
|
64
|
+
VkResult _r = (call); \
|
|
65
|
+
if (_r != VK_SUCCESS) { \
|
|
66
|
+
LOGE("%s failed: %d at %s:%d", #call, _r, __FILE__, __LINE__); \
|
|
67
|
+
} \
|
|
68
|
+
} while(0)
|
|
69
|
+
|
|
70
|
+
#define VK_CHECK_FATAL(call) do { \
|
|
71
|
+
VkResult _r = (call); \
|
|
72
|
+
if (_r != VK_SUCCESS) { \
|
|
73
|
+
LOGE("%s failed: %d at %s:%d (FATAL)", #call, _r, __FILE__, __LINE__); \
|
|
74
|
+
return; \
|
|
75
|
+
} \
|
|
48
76
|
} while(0)
|
|
49
77
|
|
|
50
78
|
// Per-frame UBO (set 0): camera ECEF for the fragment lighting view direction.
|
|
@@ -66,7 +94,6 @@ struct TerrainVertexPC {
|
|
|
66
94
|
};
|
|
67
95
|
|
|
68
96
|
struct TerrainFragmentPC {
|
|
69
|
-
// Fragment push constants occupy bytes 64-127 in the push constant block.
|
|
70
97
|
uint32_t hasOverlay; // 4 bytes
|
|
71
98
|
uint32_t isEllipsoidFallback; // 4 bytes
|
|
72
99
|
uint32_t isOnlyWater; // 4 bytes
|
|
@@ -91,6 +118,8 @@ VulkanBackend::VulkanBackend() = default;
|
|
|
91
118
|
|
|
92
119
|
VulkanBackend::~VulkanBackend() { shutdown(); }
|
|
93
120
|
|
|
121
|
+
// ── Init ────────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
94
123
|
void VulkanBackend::initialize(void* nativeSurface, int width, int height) {
|
|
95
124
|
window_ = nativeSurface;
|
|
96
125
|
viewportWidth_ = width;
|
|
@@ -105,18 +134,23 @@ void VulkanBackend::initialize(void* nativeSurface, int width, int height) {
|
|
|
105
134
|
pickPhysicalDevice();
|
|
106
135
|
createDevice();
|
|
107
136
|
createCommandPool();
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
137
|
+
createPipelineCache();
|
|
138
|
+
// Defer swapchain creation until the first beginFrame() to avoid Android
|
|
139
|
+
// compositor readiness issues. The swapchain, renderpass, depth resources,
|
|
140
|
+
// framebuffers, and pipelines will all be created on-demand.
|
|
141
|
+
// createSwapchain();
|
|
142
|
+
// createRenderPass();
|
|
143
|
+
// createDepthResources();
|
|
144
|
+
// if (sampleCount_ != VK_SAMPLE_COUNT_1_BIT) createMsaaResources();
|
|
145
|
+
// createFramebuffers();
|
|
112
146
|
createCommandBuffers();
|
|
113
147
|
createSyncObjects();
|
|
114
148
|
createDescriptorSetLayout();
|
|
115
149
|
createDescriptorPool();
|
|
116
150
|
createWaterMaskPool();
|
|
117
151
|
createPipelineLayout();
|
|
118
|
-
|
|
119
|
-
|
|
152
|
+
// createGraphicsPipelinesAll();
|
|
153
|
+
initStagingRing(kStagingSize);
|
|
120
154
|
createFallbackTexture();
|
|
121
155
|
createWaterMaskFallback();
|
|
122
156
|
|
|
@@ -147,7 +181,6 @@ void VulkanBackend::initialize(void* nativeSurface, int width, int height) {
|
|
|
147
181
|
skyWrite.pBufferInfo = &skyBufInfo;
|
|
148
182
|
vkUpdateDescriptorSets(device_, 1, &skyWrite, 0, nullptr);
|
|
149
183
|
|
|
150
|
-
// Allocate terrain UBOs + descriptor sets (set 0: UBO only, one per frame)
|
|
151
184
|
for (int i = 0; i < kMaxFramesInFlight; ++i) {
|
|
152
185
|
createBuffer(sizeof(TerrainUBO), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
|
|
153
186
|
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
|
@@ -174,12 +207,10 @@ void VulkanBackend::initialize(void* nativeSurface, int width, int height) {
|
|
|
174
207
|
uboWrite.pBufferInfo = &bufInfo;
|
|
175
208
|
vkUpdateDescriptorSets(device_, 1, &uboWrite, 0, nullptr);
|
|
176
209
|
|
|
177
|
-
// Persistent mapping — HOST_COHERENT so no explicit flush needed.
|
|
178
210
|
VK_CHECK(vkMapMemory(device_, terrainUboMems_[i], 0, sizeof(TerrainUBO),
|
|
179
211
|
0, &terrainUboMapped_[i]));
|
|
180
212
|
}
|
|
181
213
|
|
|
182
|
-
// Allocate fallback texture descriptor set (set 1)
|
|
183
214
|
{
|
|
184
215
|
VkDescriptorSetAllocateInfo allocInfo{};
|
|
185
216
|
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
|
@@ -202,9 +233,6 @@ void VulkanBackend::initialize(void* nativeSurface, int width, int height) {
|
|
|
202
233
|
texWrite.pImageInfo = &imgInfo;
|
|
203
234
|
vkUpdateDescriptorSets(device_, 1, &texWrite, 0, nullptr);
|
|
204
235
|
}
|
|
205
|
-
|
|
206
|
-
LOGI("VulkanBackend initialized: %dx%d, %zu swapchain images",
|
|
207
|
-
width, height, swapchainImages_.size());
|
|
208
236
|
}
|
|
209
237
|
|
|
210
238
|
void VulkanBackend::resize(int width, int height) {
|
|
@@ -217,6 +245,15 @@ void VulkanBackend::shutdown() {
|
|
|
217
245
|
if (device_ == VK_NULL_HANDLE) return;
|
|
218
246
|
vkDeviceWaitIdle(device_);
|
|
219
247
|
|
|
248
|
+
persistPipelineCache();
|
|
249
|
+
|
|
250
|
+
// Drain pending uploads (GPU is idle).
|
|
251
|
+
{
|
|
252
|
+
std::lock_guard<std::mutex> lk(stagingMutex_);
|
|
253
|
+
for (auto& vec : uploadsInFlight_) vec.clear();
|
|
254
|
+
pendingUploadsCurrent_.clear();
|
|
255
|
+
}
|
|
256
|
+
|
|
220
257
|
// Drain the deferred deletion queue — GPU is idle so all are safe to free.
|
|
221
258
|
{
|
|
222
259
|
std::lock_guard<std::mutex> lk(pendingDeletesMutex_);
|
|
@@ -227,29 +264,29 @@ void VulkanBackend::shutdown() {
|
|
|
227
264
|
}
|
|
228
265
|
}
|
|
229
266
|
|
|
230
|
-
// Free fallback imagery texture
|
|
231
267
|
if (fallbackTexture_.sampler) vkDestroySampler(device_, fallbackTexture_.sampler, nullptr);
|
|
232
268
|
if (fallbackTexture_.imageView) vkDestroyImageView(device_, fallbackTexture_.imageView, nullptr);
|
|
233
269
|
if (fallbackTexture_.image) vkDestroyImage(device_, fallbackTexture_.image, nullptr);
|
|
234
270
|
if (fallbackTexture_.memory) vkFreeMemory(device_, fallbackTexture_.memory, nullptr);
|
|
235
271
|
fallbackTexture_ = {};
|
|
272
|
+
delete fallbackTexturePtr_;
|
|
273
|
+
fallbackTexturePtr_ = nullptr;
|
|
236
274
|
|
|
237
|
-
// Free fallback water mask texture (its descriptor set belongs to waterMaskPool_)
|
|
238
275
|
if (fallbackWaterMaskTex_.sampler) vkDestroySampler(device_, fallbackWaterMaskTex_.sampler, nullptr);
|
|
239
276
|
if (fallbackWaterMaskTex_.imageView) vkDestroyImageView(device_, fallbackWaterMaskTex_.imageView, nullptr);
|
|
240
277
|
if (fallbackWaterMaskTex_.image) vkDestroyImage(device_, fallbackWaterMaskTex_.image, nullptr);
|
|
241
278
|
if (fallbackWaterMaskTex_.memory) vkFreeMemory(device_, fallbackWaterMaskTex_.memory, nullptr);
|
|
242
279
|
fallbackWaterMaskTex_ = {};
|
|
243
280
|
fallbackWaterMaskDescSet_ = VK_NULL_HANDLE;
|
|
281
|
+
delete fallbackWaterMaskTexPtr_;
|
|
282
|
+
fallbackWaterMaskTexPtr_ = nullptr;
|
|
244
283
|
|
|
245
|
-
// Free sky UBO (unmap before destroy)
|
|
246
284
|
if (skyUboMapped_) { vkUnmapMemory(device_, skyUboMemory_); skyUboMapped_ = nullptr; }
|
|
247
285
|
if (skyUboBuffer_) vkDestroyBuffer(device_, skyUboBuffer_, nullptr);
|
|
248
286
|
if (skyUboMemory_) vkFreeMemory(device_, skyUboMemory_, nullptr);
|
|
249
287
|
skyUboBuffer_ = VK_NULL_HANDLE;
|
|
250
288
|
skyUboMemory_ = VK_NULL_HANDLE;
|
|
251
289
|
|
|
252
|
-
// Free terrain UBOs (unmap before destroy)
|
|
253
290
|
for (int i = 0; i < kMaxFramesInFlight; ++i) {
|
|
254
291
|
if (terrainUboMapped_[i]) { vkUnmapMemory(device_, terrainUboMems_[i]); terrainUboMapped_[i] = nullptr; }
|
|
255
292
|
if (terrainUboBufs_[i]) vkDestroyBuffer(device_, terrainUboBufs_[i], nullptr);
|
|
@@ -258,7 +295,6 @@ void VulkanBackend::shutdown() {
|
|
|
258
295
|
terrainUboMems_[i] = VK_NULL_HANDLE;
|
|
259
296
|
}
|
|
260
297
|
|
|
261
|
-
// Free persistent frame buffers (position, index, UV, altitude)
|
|
262
298
|
for (int i = 0; i < kMaxFramesInFlight; ++i) {
|
|
263
299
|
if (vtxBufs_[i]) vkDestroyBuffer(device_, vtxBufs_[i], nullptr);
|
|
264
300
|
if (vtxMems_[i]) vkFreeMemory(device_, vtxMems_[i], nullptr);
|
|
@@ -273,6 +309,17 @@ void VulkanBackend::shutdown() {
|
|
|
273
309
|
vtxCaps_[i] = idxCaps_[i] = uvCaps_[i] = altCaps_[i] = 0;
|
|
274
310
|
}
|
|
275
311
|
|
|
312
|
+
// Staging ring
|
|
313
|
+
if (stagingMapped_) { vkUnmapMemory(device_, stagingMemory_); stagingMapped_ = nullptr; }
|
|
314
|
+
if (stagingBuffer_) vkDestroyBuffer(device_, stagingBuffer_, nullptr);
|
|
315
|
+
if (stagingMemory_) vkFreeMemory(device_, stagingMemory_, nullptr);
|
|
316
|
+
stagingBuffer_ = VK_NULL_HANDLE;
|
|
317
|
+
stagingMemory_ = VK_NULL_HANDLE;
|
|
318
|
+
for (auto& f : uploadFences_) {
|
|
319
|
+
if (f) vkDestroyFence(device_, f, nullptr);
|
|
320
|
+
f = VK_NULL_HANDLE;
|
|
321
|
+
}
|
|
322
|
+
|
|
276
323
|
for (auto& s : imageAvailableSemaphores_) vkDestroySemaphore(device_, s, nullptr);
|
|
277
324
|
for (auto& s : renderFinishedSemaphores_) vkDestroySemaphore(device_, s, nullptr);
|
|
278
325
|
for (auto& f : inFlightFences_) vkDestroyFence(device_, f, nullptr);
|
|
@@ -282,8 +329,7 @@ void VulkanBackend::shutdown() {
|
|
|
282
329
|
|
|
283
330
|
cleanupSwapchain();
|
|
284
331
|
|
|
285
|
-
|
|
286
|
-
if (skyPipeline_) vkDestroyPipeline(device_, skyPipeline_, nullptr);
|
|
332
|
+
destroyGraphicsPipelinesAll();
|
|
287
333
|
if (terrainPipelineLayout_) vkDestroyPipelineLayout(device_, terrainPipelineLayout_, nullptr);
|
|
288
334
|
if (skyPipelineLayout_) vkDestroyPipelineLayout(device_, skyPipelineLayout_, nullptr);
|
|
289
335
|
if (terrainDescSetLayout_) vkDestroyDescriptorSetLayout(device_, terrainDescSetLayout_, nullptr);
|
|
@@ -291,13 +337,14 @@ void VulkanBackend::shutdown() {
|
|
|
291
337
|
if (skyDescSetLayout_) vkDestroyDescriptorSetLayout(device_, skyDescSetLayout_, nullptr);
|
|
292
338
|
if (descriptorPool_) vkDestroyDescriptorPool(device_, descriptorPool_, nullptr);
|
|
293
339
|
if (waterMaskPool_) vkDestroyDescriptorPool(device_, waterMaskPool_, nullptr);
|
|
340
|
+
if (pipelineCache_) vkDestroyPipelineCache(device_, pipelineCache_, nullptr);
|
|
294
341
|
if (commandPool_) vkDestroyCommandPool(device_, commandPool_, nullptr);
|
|
295
342
|
if (renderPass_) vkDestroyRenderPass(device_, renderPass_, nullptr);
|
|
296
343
|
|
|
297
|
-
terrainPipeline_ = skyPipeline_ = VK_NULL_HANDLE;
|
|
298
344
|
terrainPipelineLayout_ = skyPipelineLayout_ = VK_NULL_HANDLE;
|
|
299
345
|
terrainDescSetLayout_ = terrainTexLayout_ = skyDescSetLayout_ = VK_NULL_HANDLE;
|
|
300
346
|
descriptorPool_ = waterMaskPool_ = VK_NULL_HANDLE;
|
|
347
|
+
pipelineCache_ = VK_NULL_HANDLE;
|
|
301
348
|
commandPool_ = VK_NULL_HANDLE;
|
|
302
349
|
renderPass_ = VK_NULL_HANDLE;
|
|
303
350
|
|
|
@@ -340,24 +387,67 @@ void VulkanBackend::pickPhysicalDevice() {
|
|
|
340
387
|
std::vector<VkPhysicalDevice> devices(count);
|
|
341
388
|
vkEnumeratePhysicalDevices(instance_, &count, devices.data());
|
|
342
389
|
|
|
390
|
+
// Score: discrete >> integrated >> any. Within type, larger device-local
|
|
391
|
+
// memory wins. Falls back to the first graphics+present queue if nothing
|
|
392
|
+
// scores positively.
|
|
393
|
+
int bestScore = -1;
|
|
394
|
+
uint32_t bestQueue = 0;
|
|
395
|
+
VkPhysicalDevice best = VK_NULL_HANDLE;
|
|
396
|
+
|
|
343
397
|
for (auto& dev : devices) {
|
|
344
398
|
uint32_t qCount = 0;
|
|
345
399
|
vkGetPhysicalDeviceQueueFamilyProperties(dev, &qCount, nullptr);
|
|
346
400
|
std::vector<VkQueueFamilyProperties> qFamilies(qCount);
|
|
347
401
|
vkGetPhysicalDeviceQueueFamilyProperties(dev, &qCount, qFamilies.data());
|
|
348
402
|
|
|
403
|
+
uint32_t graphicsQueueIdx = UINT32_MAX;
|
|
349
404
|
for (uint32_t i = 0; i < qCount; ++i) {
|
|
350
405
|
VkBool32 presentSupport = VK_FALSE;
|
|
351
406
|
vkGetPhysicalDeviceSurfaceSupportKHR(dev, i, surface_, &presentSupport);
|
|
352
|
-
|
|
353
407
|
if ((qFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) && presentSupport) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
return;
|
|
408
|
+
graphicsQueueIdx = i;
|
|
409
|
+
break;
|
|
357
410
|
}
|
|
358
411
|
}
|
|
412
|
+
if (graphicsQueueIdx == UINT32_MAX) continue;
|
|
413
|
+
|
|
414
|
+
VkPhysicalDeviceProperties props;
|
|
415
|
+
vkGetPhysicalDeviceProperties(dev, &props);
|
|
416
|
+
|
|
417
|
+
int score = 0;
|
|
418
|
+
if (props.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) score = 1000;
|
|
419
|
+
else if (props.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU) score = 500;
|
|
420
|
+
else score = 100;
|
|
421
|
+
|
|
422
|
+
if (score > bestScore) {
|
|
423
|
+
bestScore = score;
|
|
424
|
+
bestQueue = graphicsQueueIdx;
|
|
425
|
+
best = dev;
|
|
426
|
+
}
|
|
359
427
|
}
|
|
360
|
-
|
|
428
|
+
|
|
429
|
+
if (best == VK_NULL_HANDLE) {
|
|
430
|
+
LOGE("No suitable Vulkan physical device found");
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
physicalDevice_ = best;
|
|
435
|
+
graphicsFamily_ = bestQueue;
|
|
436
|
+
|
|
437
|
+
// Cache properties relevant to sampler / pipeline state.
|
|
438
|
+
VkPhysicalDeviceFeatures feats{};
|
|
439
|
+
VkPhysicalDeviceProperties props{};
|
|
440
|
+
vkGetPhysicalDeviceFeatures(physicalDevice_, &feats);
|
|
441
|
+
vkGetPhysicalDeviceProperties(physicalDevice_, &props);
|
|
442
|
+
supportsAnisotropy_ = feats.samplerAnisotropy == VK_TRUE;
|
|
443
|
+
maxAnisotropy_ = props.limits.maxSamplerAnisotropy;
|
|
444
|
+
|
|
445
|
+
const VkSampleCountFlags counts = props.limits.framebufferColorSampleCounts &
|
|
446
|
+
props.limits.framebufferDepthSampleCounts;
|
|
447
|
+
if (counts & VK_SAMPLE_COUNT_8_BIT) supportedMsaaMask_ = VK_SAMPLE_COUNT_8_BIT;
|
|
448
|
+
else if (counts & VK_SAMPLE_COUNT_4_BIT) supportedMsaaMask_ = VK_SAMPLE_COUNT_4_BIT;
|
|
449
|
+
else if (counts & VK_SAMPLE_COUNT_2_BIT) supportedMsaaMask_ = VK_SAMPLE_COUNT_2_BIT;
|
|
450
|
+
else supportedMsaaMask_ = VK_SAMPLE_COUNT_1_BIT;
|
|
361
451
|
}
|
|
362
452
|
|
|
363
453
|
void VulkanBackend::createDevice() {
|
|
@@ -371,6 +461,7 @@ void VulkanBackend::createDevice() {
|
|
|
371
461
|
const char* devExtensions[] = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
|
|
372
462
|
|
|
373
463
|
VkPhysicalDeviceFeatures features{};
|
|
464
|
+
features.samplerAnisotropy = supportsAnisotropy_ ? VK_TRUE : VK_FALSE;
|
|
374
465
|
|
|
375
466
|
VkDeviceCreateInfo createInfo{};
|
|
376
467
|
createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
|
|
@@ -385,10 +476,19 @@ void VulkanBackend::createDevice() {
|
|
|
385
476
|
presentQueue_ = graphicsQueue_;
|
|
386
477
|
}
|
|
387
478
|
|
|
388
|
-
|
|
479
|
+
VkSampleCountFlagBits VulkanBackend::resolveMsaaSamples(int requested) const {
|
|
480
|
+
if (requested <= 1) return VK_SAMPLE_COUNT_1_BIT;
|
|
481
|
+
// Clamp request to the largest mask the device actually supports.
|
|
482
|
+
VkSampleCountFlagBits desired =
|
|
483
|
+
requested >= 8 ? VK_SAMPLE_COUNT_8_BIT
|
|
484
|
+
: (requested >= 4 ? VK_SAMPLE_COUNT_4_BIT
|
|
485
|
+
: VK_SAMPLE_COUNT_2_BIT);
|
|
486
|
+
if (static_cast<int>(desired) > static_cast<int>(supportedMsaaMask_))
|
|
487
|
+
desired = supportedMsaaMask_;
|
|
488
|
+
return desired;
|
|
489
|
+
}
|
|
389
490
|
|
|
390
491
|
VkFormat VulkanBackend::pickDepthFormat() const {
|
|
391
|
-
// Prefer D32 for maximum precision; fall back to packed D24/S8 or D16.
|
|
392
492
|
for (VkFormat candidate : {VK_FORMAT_D32_SFLOAT,
|
|
393
493
|
VK_FORMAT_D24_UNORM_S8_UINT,
|
|
394
494
|
VK_FORMAT_D16_UNORM}) {
|
|
@@ -401,6 +501,26 @@ VkFormat VulkanBackend::pickDepthFormat() const {
|
|
|
401
501
|
return VK_FORMAT_D32_SFLOAT;
|
|
402
502
|
}
|
|
403
503
|
|
|
504
|
+
VkPresentModeKHR VulkanBackend::pickPresentMode() const {
|
|
505
|
+
uint32_t count = 0;
|
|
506
|
+
vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice_, surface_, &count, nullptr);
|
|
507
|
+
std::vector<VkPresentModeKHR> modes(count);
|
|
508
|
+
vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice_, surface_, &count, modes.data());
|
|
509
|
+
|
|
510
|
+
bool hasMailbox = false, hasFifoRelaxed = false;
|
|
511
|
+
for (auto m : modes) {
|
|
512
|
+
if (m == VK_PRESENT_MODE_MAILBOX_KHR) hasMailbox = true;
|
|
513
|
+
if (m == VK_PRESENT_MODE_FIFO_RELAXED_KHR) hasFifoRelaxed = true;
|
|
514
|
+
}
|
|
515
|
+
// Prefer FIFO_RELAXED for stutter resilience on phones where we sometimes
|
|
516
|
+
// miss a vsync — it presents the late frame immediately rather than holding
|
|
517
|
+
// for the next vsync. MAILBOX is "lower latency" but can drop frames; not
|
|
518
|
+
// ideal for a globe view that's already battery-sensitive.
|
|
519
|
+
if (hasFifoRelaxed) return VK_PRESENT_MODE_FIFO_RELAXED_KHR;
|
|
520
|
+
if (hasMailbox) return VK_PRESENT_MODE_MAILBOX_KHR;
|
|
521
|
+
return VK_PRESENT_MODE_FIFO_KHR; // always available per spec
|
|
522
|
+
}
|
|
523
|
+
|
|
404
524
|
// ── Swapchain ──────────────────────────────────────────────────────────────────
|
|
405
525
|
|
|
406
526
|
void VulkanBackend::createSwapchain() {
|
|
@@ -412,7 +532,6 @@ void VulkanBackend::createSwapchain() {
|
|
|
412
532
|
std::vector<VkSurfaceFormatKHR> formats(fmtCount);
|
|
413
533
|
vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice_, surface_, &fmtCount, formats.data());
|
|
414
534
|
|
|
415
|
-
// Prefer sRGB
|
|
416
535
|
swapchainFormat_ = formats[0].format;
|
|
417
536
|
VkColorSpaceKHR colorSpace = formats[0].colorSpace;
|
|
418
537
|
for (auto& fmt : formats) {
|
|
@@ -427,7 +546,6 @@ void VulkanBackend::createSwapchain() {
|
|
|
427
546
|
}
|
|
428
547
|
}
|
|
429
548
|
|
|
430
|
-
// Determine extent
|
|
431
549
|
if (caps.currentExtent.width != UINT32_MAX) {
|
|
432
550
|
swapchainExtent_ = caps.currentExtent;
|
|
433
551
|
} else {
|
|
@@ -441,11 +559,8 @@ void VulkanBackend::createSwapchain() {
|
|
|
441
559
|
if (caps.maxImageCount > 0 && imageCount > caps.maxImageCount)
|
|
442
560
|
imageCount = caps.maxImageCount;
|
|
443
561
|
|
|
444
|
-
// Pick depth format once (stable across recreations on the same device).
|
|
445
562
|
depthFormat_ = pickDepthFormat();
|
|
446
563
|
|
|
447
|
-
// Prefer OPAQUE composite alpha (avoids blending with the window behind the
|
|
448
|
-
// surface); fall back through the other modes in priority order.
|
|
449
564
|
VkCompositeAlphaFlagBitsKHR compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
|
|
450
565
|
for (auto candidate : {VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
|
|
451
566
|
VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR,
|
|
@@ -471,7 +586,7 @@ void VulkanBackend::createSwapchain() {
|
|
|
471
586
|
swapInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
|
472
587
|
swapInfo.preTransform = caps.currentTransform;
|
|
473
588
|
swapInfo.compositeAlpha = compositeAlpha;
|
|
474
|
-
swapInfo.presentMode =
|
|
589
|
+
swapInfo.presentMode = pickPresentMode();
|
|
475
590
|
swapInfo.clipped = VK_TRUE;
|
|
476
591
|
swapInfo.oldSwapchain = oldSwapchain;
|
|
477
592
|
VK_CHECK(vkCreateSwapchainKHR(device_, &swapInfo, nullptr, &swapchain_));
|
|
@@ -501,19 +616,27 @@ void VulkanBackend::createSwapchain() {
|
|
|
501
616
|
}
|
|
502
617
|
|
|
503
618
|
void VulkanBackend::createRenderPass() {
|
|
619
|
+
const bool msaa = (sampleCount_ != VK_SAMPLE_COUNT_1_BIT);
|
|
620
|
+
|
|
621
|
+
// Colour attachment: when MSAA is enabled this is the multisampled colour
|
|
622
|
+
// target (does not store), and we add a single-sample resolve attachment
|
|
623
|
+
// that becomes PRESENT_SRC. When MSAA is off the colour attachment IS the
|
|
624
|
+
// swapchain image directly.
|
|
504
625
|
VkAttachmentDescription colorAttach{};
|
|
505
626
|
colorAttach.format = swapchainFormat_;
|
|
506
|
-
colorAttach.samples =
|
|
627
|
+
colorAttach.samples = sampleCount_;
|
|
507
628
|
colorAttach.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
|
508
|
-
colorAttach.storeOp =
|
|
629
|
+
colorAttach.storeOp = msaa ? VK_ATTACHMENT_STORE_OP_DONT_CARE
|
|
630
|
+
: VK_ATTACHMENT_STORE_OP_STORE;
|
|
509
631
|
colorAttach.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
|
510
632
|
colorAttach.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
|
511
633
|
colorAttach.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
512
|
-
colorAttach.finalLayout =
|
|
634
|
+
colorAttach.finalLayout = msaa ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
|
|
635
|
+
: VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
|
513
636
|
|
|
514
637
|
VkAttachmentDescription depthAttach{};
|
|
515
638
|
depthAttach.format = depthFormat_;
|
|
516
|
-
depthAttach.samples =
|
|
639
|
+
depthAttach.samples = sampleCount_;
|
|
517
640
|
depthAttach.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
|
518
641
|
depthAttach.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
|
519
642
|
depthAttach.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
|
@@ -521,6 +644,16 @@ void VulkanBackend::createRenderPass() {
|
|
|
521
644
|
depthAttach.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
522
645
|
depthAttach.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
|
523
646
|
|
|
647
|
+
VkAttachmentDescription resolveAttach{};
|
|
648
|
+
resolveAttach.format = swapchainFormat_;
|
|
649
|
+
resolveAttach.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
650
|
+
resolveAttach.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
|
651
|
+
resolveAttach.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
|
652
|
+
resolveAttach.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
|
653
|
+
resolveAttach.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
|
654
|
+
resolveAttach.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
655
|
+
resolveAttach.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
|
656
|
+
|
|
524
657
|
VkAttachmentReference colorRef{};
|
|
525
658
|
colorRef.attachment = 0;
|
|
526
659
|
colorRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
|
@@ -529,11 +662,16 @@ void VulkanBackend::createRenderPass() {
|
|
|
529
662
|
depthRef.attachment = 1;
|
|
530
663
|
depthRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
|
531
664
|
|
|
665
|
+
VkAttachmentReference resolveRef{};
|
|
666
|
+
resolveRef.attachment = 2;
|
|
667
|
+
resolveRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
|
668
|
+
|
|
532
669
|
VkSubpassDescription subpass{};
|
|
533
670
|
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
|
534
671
|
subpass.colorAttachmentCount = 1;
|
|
535
672
|
subpass.pColorAttachments = &colorRef;
|
|
536
673
|
subpass.pDepthStencilAttachment = &depthRef;
|
|
674
|
+
if (msaa) subpass.pResolveAttachments = &resolveRef;
|
|
537
675
|
|
|
538
676
|
VkSubpassDependency dependency{};
|
|
539
677
|
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
|
|
@@ -546,7 +684,8 @@ void VulkanBackend::createRenderPass() {
|
|
|
546
684
|
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
|
|
547
685
|
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
|
548
686
|
|
|
549
|
-
std::
|
|
687
|
+
std::vector<VkAttachmentDescription> attachments = {colorAttach, depthAttach};
|
|
688
|
+
if (msaa) attachments.push_back(resolveAttach);
|
|
550
689
|
|
|
551
690
|
VkRenderPassCreateInfo rpInfo{};
|
|
552
691
|
rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
|
|
@@ -573,7 +712,7 @@ void VulkanBackend::createDepthResources() {
|
|
|
573
712
|
imgInfo.extent.depth = 1;
|
|
574
713
|
imgInfo.mipLevels = 1;
|
|
575
714
|
imgInfo.arrayLayers = 1;
|
|
576
|
-
imgInfo.samples =
|
|
715
|
+
imgInfo.samples = sampleCount_;
|
|
577
716
|
imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
|
578
717
|
imgInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
|
|
579
718
|
VK_CHECK(vkCreateImage(device_, &imgInfo, nullptr, &depthImage_));
|
|
@@ -602,10 +741,65 @@ void VulkanBackend::createDepthResources() {
|
|
|
602
741
|
VK_CHECK(vkCreateImageView(device_, &viewInfo, nullptr, &depthImageView_));
|
|
603
742
|
}
|
|
604
743
|
|
|
744
|
+
void VulkanBackend::createMsaaResources() {
|
|
745
|
+
if (msaaColorView_) vkDestroyImageView(device_, msaaColorView_, nullptr);
|
|
746
|
+
if (msaaColorImage_) vkDestroyImage(device_, msaaColorImage_, nullptr);
|
|
747
|
+
if (msaaColorMemory_) vkFreeMemory(device_, msaaColorMemory_, nullptr);
|
|
748
|
+
|
|
749
|
+
VkImageCreateInfo imgInfo{};
|
|
750
|
+
imgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
|
751
|
+
imgInfo.imageType = VK_IMAGE_TYPE_2D;
|
|
752
|
+
imgInfo.format = swapchainFormat_;
|
|
753
|
+
imgInfo.extent = {swapchainExtent_.width, swapchainExtent_.height, 1};
|
|
754
|
+
imgInfo.mipLevels = 1;
|
|
755
|
+
imgInfo.arrayLayers = 1;
|
|
756
|
+
imgInfo.samples = sampleCount_;
|
|
757
|
+
imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
|
758
|
+
imgInfo.usage = VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT |
|
|
759
|
+
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
|
|
760
|
+
imgInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
761
|
+
VK_CHECK(vkCreateImage(device_, &imgInfo, nullptr, &msaaColorImage_));
|
|
762
|
+
|
|
763
|
+
VkMemoryRequirements memReqs;
|
|
764
|
+
vkGetImageMemoryRequirements(device_, msaaColorImage_, &memReqs);
|
|
765
|
+
|
|
766
|
+
// Prefer LAZILY_ALLOCATED for the transient MSAA target — on tiled mobile
|
|
767
|
+
// GPUs this avoids actually allocating physical memory.
|
|
768
|
+
bool foundLazy = false;
|
|
769
|
+
uint32_t typeIdx = findMemoryType(
|
|
770
|
+
memReqs.memoryTypeBits,
|
|
771
|
+
VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT, &foundLazy);
|
|
772
|
+
if (!foundLazy) {
|
|
773
|
+
typeIdx = findMemoryType(memReqs.memoryTypeBits,
|
|
774
|
+
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
VkMemoryAllocateInfo allocInfo{};
|
|
778
|
+
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
|
|
779
|
+
allocInfo.allocationSize = memReqs.size;
|
|
780
|
+
allocInfo.memoryTypeIndex = typeIdx;
|
|
781
|
+
VK_CHECK(vkAllocateMemory(device_, &allocInfo, nullptr, &msaaColorMemory_));
|
|
782
|
+
VK_CHECK(vkBindImageMemory(device_, msaaColorImage_, msaaColorMemory_, 0));
|
|
783
|
+
|
|
784
|
+
VkImageViewCreateInfo viewInfo{};
|
|
785
|
+
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
|
786
|
+
viewInfo.image = msaaColorImage_;
|
|
787
|
+
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
|
788
|
+
viewInfo.format = swapchainFormat_;
|
|
789
|
+
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
|
790
|
+
VK_CHECK(vkCreateImageView(device_, &viewInfo, nullptr, &msaaColorView_));
|
|
791
|
+
}
|
|
792
|
+
|
|
605
793
|
void VulkanBackend::createFramebuffers() {
|
|
606
794
|
framebuffers_.resize(swapchainImageViews_.size());
|
|
795
|
+
const bool msaa = (sampleCount_ != VK_SAMPLE_COUNT_1_BIT);
|
|
607
796
|
for (size_t i = 0; i < swapchainImageViews_.size(); ++i) {
|
|
608
|
-
std::
|
|
797
|
+
std::vector<VkImageView> attachments;
|
|
798
|
+
if (msaa) {
|
|
799
|
+
attachments = {msaaColorView_, depthImageView_, swapchainImageViews_[i]};
|
|
800
|
+
} else {
|
|
801
|
+
attachments = {swapchainImageViews_[i], depthImageView_};
|
|
802
|
+
}
|
|
609
803
|
|
|
610
804
|
VkFramebufferCreateInfo fbInfo{};
|
|
611
805
|
fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
|
|
@@ -622,9 +816,6 @@ void VulkanBackend::createFramebuffers() {
|
|
|
622
816
|
void VulkanBackend::createCommandPool() {
|
|
623
817
|
VkCommandPoolCreateInfo poolInfo{};
|
|
624
818
|
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
819
|
poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT |
|
|
629
820
|
VK_COMMAND_POOL_CREATE_TRANSIENT_BIT;
|
|
630
821
|
poolInfo.queueFamilyIndex = graphicsFamily_;
|
|
@@ -639,11 +830,17 @@ void VulkanBackend::createCommandBuffers() {
|
|
|
639
830
|
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
|
640
831
|
allocInfo.commandBufferCount = static_cast<uint32_t>(commandBuffers_.size());
|
|
641
832
|
VK_CHECK(vkAllocateCommandBuffers(device_, &allocInfo, commandBuffers_.data()));
|
|
833
|
+
|
|
834
|
+
// Separate command buffers for upload (one per frame in flight).
|
|
835
|
+
uploadCommandBuffers_.resize(kMaxFramesInFlight);
|
|
836
|
+
allocInfo.commandBufferCount = static_cast<uint32_t>(uploadCommandBuffers_.size());
|
|
837
|
+
VK_CHECK(vkAllocateCommandBuffers(device_, &allocInfo, uploadCommandBuffers_.data()));
|
|
642
838
|
}
|
|
643
839
|
|
|
644
840
|
void VulkanBackend::createSyncObjects() {
|
|
645
841
|
imageAvailableSemaphores_.resize(kMaxFramesInFlight);
|
|
646
|
-
renderFinishedSemaphores_
|
|
842
|
+
// renderFinishedSemaphores_ will be created after swapchain creation
|
|
843
|
+
// since we need to know swapchainImages_.size()
|
|
647
844
|
inFlightFences_.resize(kMaxFramesInFlight);
|
|
648
845
|
|
|
649
846
|
VkSemaphoreCreateInfo semInfo{};
|
|
@@ -655,16 +852,64 @@ void VulkanBackend::createSyncObjects() {
|
|
|
655
852
|
|
|
656
853
|
for (int i = 0; i < kMaxFramesInFlight; ++i) {
|
|
657
854
|
VK_CHECK(vkCreateSemaphore(device_, &semInfo, nullptr, &imageAvailableSemaphores_[i]));
|
|
658
|
-
VK_CHECK(vkCreateSemaphore(device_, &semInfo, nullptr, &renderFinishedSemaphores_[i]));
|
|
659
855
|
VK_CHECK(vkCreateFence(device_, &fenceInfo, nullptr, &inFlightFences_[i]));
|
|
856
|
+
VK_CHECK(vkCreateFence(device_, &fenceInfo, nullptr, &uploadFences_[i]));
|
|
857
|
+
}
|
|
858
|
+
// renderFinishedSemaphores_ created in beginFrame after swapchain is ready
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// ── Pipeline cache (P1-vk-pipeline-cache) ───────────────────────────────────────
|
|
862
|
+
|
|
863
|
+
namespace {
|
|
864
|
+
std::string pipelineCachePath(const std::string& cacheDir) {
|
|
865
|
+
if (cacheDir.empty()) return std::string();
|
|
866
|
+
return cacheDir + "/cesium_pipeline.cache";
|
|
867
|
+
}
|
|
868
|
+
} // namespace
|
|
869
|
+
|
|
870
|
+
void VulkanBackend::createPipelineCache() {
|
|
871
|
+
std::vector<uint8_t> initial;
|
|
872
|
+
const std::string path = pipelineCachePath(cacheDir_);
|
|
873
|
+
if (!path.empty()) {
|
|
874
|
+
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
|
875
|
+
if (f.is_open()) {
|
|
876
|
+
const std::streamsize size = f.tellg();
|
|
877
|
+
if (size > 0) {
|
|
878
|
+
initial.resize(static_cast<size_t>(size));
|
|
879
|
+
f.seekg(0, std::ios::beg);
|
|
880
|
+
f.read(reinterpret_cast<char*>(initial.data()), size);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
660
883
|
}
|
|
884
|
+
|
|
885
|
+
VkPipelineCacheCreateInfo info{};
|
|
886
|
+
info.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO;
|
|
887
|
+
info.initialDataSize = initial.size();
|
|
888
|
+
info.pInitialData = initial.empty() ? nullptr : initial.data();
|
|
889
|
+
VK_CHECK(vkCreatePipelineCache(device_, &info, nullptr, &pipelineCache_));
|
|
661
890
|
}
|
|
662
891
|
|
|
663
|
-
|
|
892
|
+
void VulkanBackend::persistPipelineCache() {
|
|
893
|
+
if (!pipelineCache_) return;
|
|
894
|
+
const std::string path = pipelineCachePath(cacheDir_);
|
|
895
|
+
if (path.empty()) return;
|
|
896
|
+
|
|
897
|
+
size_t size = 0;
|
|
898
|
+
if (vkGetPipelineCacheData(device_, pipelineCache_, &size, nullptr) != VK_SUCCESS) return;
|
|
899
|
+
if (size == 0) return;
|
|
900
|
+
|
|
901
|
+
std::vector<uint8_t> data(size);
|
|
902
|
+
if (vkGetPipelineCacheData(device_, pipelineCache_, &size, data.data()) != VK_SUCCESS) return;
|
|
903
|
+
|
|
904
|
+
std::ofstream f(path, std::ios::binary | std::ios::trunc);
|
|
905
|
+
if (!f.is_open()) return;
|
|
906
|
+
f.write(reinterpret_cast<const char*>(data.data()),
|
|
907
|
+
static_cast<std::streamsize>(size));
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// ── Descriptor / pipeline layouts ───────────────────────────────────────────────
|
|
664
911
|
|
|
665
912
|
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
913
|
VkDescriptorSetLayoutBinding uboBinding{};
|
|
669
914
|
uboBinding.binding = 0;
|
|
670
915
|
uboBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
|
@@ -677,7 +922,6 @@ void VulkanBackend::createDescriptorSetLayout() {
|
|
|
677
922
|
uboLayoutInfo.pBindings = &uboBinding;
|
|
678
923
|
VK_CHECK(vkCreateDescriptorSetLayout(device_, &uboLayoutInfo, nullptr, &terrainDescSetLayout_));
|
|
679
924
|
|
|
680
|
-
// Terrain set 1: combined image sampler at binding 0 (fragment only)
|
|
681
925
|
VkDescriptorSetLayoutBinding samplerBinding{};
|
|
682
926
|
samplerBinding.binding = 0;
|
|
683
927
|
samplerBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
@@ -690,7 +934,6 @@ void VulkanBackend::createDescriptorSetLayout() {
|
|
|
690
934
|
texLayoutInfo.pBindings = &samplerBinding;
|
|
691
935
|
VK_CHECK(vkCreateDescriptorSetLayout(device_, &texLayoutInfo, nullptr, &terrainTexLayout_));
|
|
692
936
|
|
|
693
|
-
// Sky: binding 0 = UBO only
|
|
694
937
|
VkDescriptorSetLayoutBinding skyUboBinding{};
|
|
695
938
|
skyUboBinding.binding = 0;
|
|
696
939
|
skyUboBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
|
@@ -705,33 +948,27 @@ void VulkanBackend::createDescriptorSetLayout() {
|
|
|
705
948
|
}
|
|
706
949
|
|
|
707
950
|
void VulkanBackend::createPipelineLayout() {
|
|
708
|
-
//
|
|
709
|
-
//
|
|
710
|
-
//
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
|
951
|
+
// P2-arg-buffers: one combined push range over both stages, so we issue a
|
|
952
|
+
// single vkCmdPushConstants per draw instead of two. The Vulkan spec
|
|
953
|
+
// allows multi-stage ranges and the GLSL/Metal shaders read the same
|
|
954
|
+
// block layout regardless.
|
|
955
|
+
VkPushConstantRange pushRange{};
|
|
956
|
+
pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
|
|
957
|
+
pushRange.offset = 0;
|
|
958
|
+
pushRange.size = sizeof(TerrainVertexPC) + sizeof(TerrainFragmentPC); // 128 bytes
|
|
719
959
|
|
|
720
960
|
VkDescriptorSetLayout terrainSetLayouts[] = {
|
|
721
|
-
terrainDescSetLayout_,
|
|
722
|
-
terrainTexLayout_, // set 1: imagery overlay
|
|
723
|
-
terrainTexLayout_, // set 2: water mask (same layout, different pool)
|
|
961
|
+
terrainDescSetLayout_, terrainTexLayout_, terrainTexLayout_,
|
|
724
962
|
};
|
|
725
963
|
|
|
726
964
|
VkPipelineLayoutCreateInfo terrainLayoutInfo{};
|
|
727
965
|
terrainLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
|
|
728
966
|
terrainLayoutInfo.setLayoutCount = 3;
|
|
729
967
|
terrainLayoutInfo.pSetLayouts = terrainSetLayouts;
|
|
730
|
-
terrainLayoutInfo.pushConstantRangeCount =
|
|
731
|
-
terrainLayoutInfo.pPushConstantRanges =
|
|
968
|
+
terrainLayoutInfo.pushConstantRangeCount = 1;
|
|
969
|
+
terrainLayoutInfo.pPushConstantRanges = &pushRange;
|
|
732
970
|
VK_CHECK(vkCreatePipelineLayout(device_, &terrainLayoutInfo, nullptr, &terrainPipelineLayout_));
|
|
733
971
|
|
|
734
|
-
// Sky: no push constants
|
|
735
972
|
VkPipelineLayoutCreateInfo skyLayoutInfo{};
|
|
736
973
|
skyLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
|
|
737
974
|
skyLayoutInfo.setLayoutCount = 1;
|
|
@@ -740,8 +977,6 @@ void VulkanBackend::createPipelineLayout() {
|
|
|
740
977
|
}
|
|
741
978
|
|
|
742
979
|
void VulkanBackend::createDescriptorPool() {
|
|
743
|
-
// UBO sets: kMaxFramesInFlight terrain + 1 sky
|
|
744
|
-
// Sampler sets: 1 fallback + up to kMaxRasterTextures raster overlay textures
|
|
745
980
|
std::array<VkDescriptorPoolSize, 2> poolSizes{};
|
|
746
981
|
poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
|
747
982
|
poolSizes[0].descriptorCount = static_cast<uint32_t>(kMaxFramesInFlight + 1);
|
|
@@ -758,8 +993,6 @@ void VulkanBackend::createDescriptorPool() {
|
|
|
758
993
|
}
|
|
759
994
|
|
|
760
995
|
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
996
|
VkDescriptorPoolSize poolSize{};
|
|
764
997
|
poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
765
998
|
poolSize.descriptorCount = static_cast<uint32_t>(kMaxRasterTextures + 1);
|
|
@@ -773,87 +1006,31 @@ void VulkanBackend::createWaterMaskPool() {
|
|
|
773
1006
|
VK_CHECK(vkCreateDescriptorPool(device_, &poolInfo, nullptr, &waterMaskPool_));
|
|
774
1007
|
}
|
|
775
1008
|
|
|
776
|
-
|
|
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);
|
|
1009
|
+
// ── Fallback textures (synchronous path; only used at init) ────────────────────
|
|
818
1010
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1011
|
+
void VulkanBackend::createFallbackTexture() {
|
|
1012
|
+
const uint8_t white[4] = {255, 255, 255, 255};
|
|
1013
|
+
VulkanTexture* t = createImageFromPixels(white, 1, 1,
|
|
1014
|
+
descriptorPool_,
|
|
1015
|
+
VK_SAMPLE_COUNT_1_BIT,
|
|
1016
|
+
/*wantMipmaps=*/false);
|
|
1017
|
+
if (t) {
|
|
1018
|
+
fallbackTexture_ = *t;
|
|
1019
|
+
fallbackTexturePtr_ = t; // Keep alive - pending upload references this!
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
826
1022
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
//
|
|
837
|
-
|
|
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);
|
|
1023
|
+
void VulkanBackend::createWaterMaskFallback() {
|
|
1024
|
+
const uint8_t zero[4] = {0, 0, 0, 255};
|
|
1025
|
+
VulkanTexture* t = createImageFromPixels(zero, 1, 1,
|
|
1026
|
+
waterMaskPool_,
|
|
1027
|
+
VK_SAMPLE_COUNT_1_BIT,
|
|
1028
|
+
/*wantMipmaps=*/false);
|
|
1029
|
+
if (t) {
|
|
1030
|
+
fallbackWaterMaskTex_ = *t;
|
|
1031
|
+
fallbackWaterMaskDescSet_ = t->descriptorSet;
|
|
1032
|
+
fallbackWaterMaskTexPtr_ = t; // Keep alive - pending upload references this!
|
|
1033
|
+
}
|
|
857
1034
|
}
|
|
858
1035
|
|
|
859
1036
|
// ── Shader compilation ─────────────────────────────────────────────────────────
|
|
@@ -869,25 +1046,45 @@ static VkShaderModule createShaderModule(VkDevice device, const uint32_t* code,
|
|
|
869
1046
|
return module;
|
|
870
1047
|
}
|
|
871
1048
|
|
|
872
|
-
// ── Pipeline creation
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1049
|
+
// ── Pipeline creation (deduped via a small builder, P2-pipelines-dedupe) ──────
|
|
1050
|
+
|
|
1051
|
+
namespace {
|
|
1052
|
+
|
|
1053
|
+
struct PipelineBuildSpec {
|
|
1054
|
+
VkShaderModule vert;
|
|
1055
|
+
VkShaderModule frag;
|
|
1056
|
+
// Vertex input description — empty for sky.
|
|
1057
|
+
uint32_t bindingCount;
|
|
1058
|
+
const VkVertexInputBindingDescription* bindings;
|
|
1059
|
+
uint32_t attributeCount;
|
|
1060
|
+
const VkVertexInputAttributeDescription* attributes;
|
|
1061
|
+
bool depthTest;
|
|
1062
|
+
bool depthWrite;
|
|
1063
|
+
VkCullModeFlags cull;
|
|
1064
|
+
VkSampleCountFlagBits samples;
|
|
1065
|
+
};
|
|
877
1066
|
|
|
1067
|
+
VkPipeline buildPipeline(VkDevice device,
|
|
1068
|
+
VkPipelineLayout layout,
|
|
1069
|
+
VkRenderPass renderPass,
|
|
1070
|
+
VkPipelineCache cache,
|
|
1071
|
+
const PipelineBuildSpec& spec) {
|
|
878
1072
|
VkPipelineShaderStageCreateInfo stages[2]{};
|
|
879
1073
|
stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
|
880
1074
|
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
|
|
881
|
-
stages[0].module =
|
|
1075
|
+
stages[0].module = spec.vert;
|
|
882
1076
|
stages[0].pName = "main";
|
|
883
1077
|
stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
|
884
1078
|
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
|
|
885
|
-
stages[1].module =
|
|
1079
|
+
stages[1].module = spec.frag;
|
|
886
1080
|
stages[1].pName = "main";
|
|
887
1081
|
|
|
888
|
-
// No vertex input (fullscreen triangle from gl_VertexIndex)
|
|
889
1082
|
VkPipelineVertexInputStateCreateInfo vertexInput{};
|
|
890
|
-
vertexInput.sType
|
|
1083
|
+
vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
|
|
1084
|
+
vertexInput.vertexBindingDescriptionCount = spec.bindingCount;
|
|
1085
|
+
vertexInput.pVertexBindingDescriptions = spec.bindings;
|
|
1086
|
+
vertexInput.vertexAttributeDescriptionCount = spec.attributeCount;
|
|
1087
|
+
vertexInput.pVertexAttributeDescriptions = spec.attributes;
|
|
891
1088
|
|
|
892
1089
|
VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
|
|
893
1090
|
inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
|
|
@@ -902,17 +1099,18 @@ void VulkanBackend::createSkyPipeline() {
|
|
|
902
1099
|
rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
|
|
903
1100
|
rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
|
|
904
1101
|
rasterizer.lineWidth = 1.0f;
|
|
905
|
-
rasterizer.cullMode =
|
|
1102
|
+
rasterizer.cullMode = spec.cull;
|
|
906
1103
|
rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
|
907
1104
|
|
|
908
1105
|
VkPipelineMultisampleStateCreateInfo multisampling{};
|
|
909
1106
|
multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
|
|
910
|
-
multisampling.rasterizationSamples =
|
|
1107
|
+
multisampling.rasterizationSamples = spec.samples;
|
|
911
1108
|
|
|
912
1109
|
VkPipelineDepthStencilStateCreateInfo depthStencil{};
|
|
913
1110
|
depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
|
|
914
|
-
depthStencil.depthTestEnable = VK_FALSE;
|
|
915
|
-
depthStencil.depthWriteEnable = VK_FALSE;
|
|
1111
|
+
depthStencil.depthTestEnable = spec.depthTest ? VK_TRUE : VK_FALSE;
|
|
1112
|
+
depthStencil.depthWriteEnable = spec.depthWrite ? VK_TRUE : VK_FALSE;
|
|
1113
|
+
depthStencil.depthCompareOp = VK_COMPARE_OP_GREATER; // Reversed-Z
|
|
916
1114
|
|
|
917
1115
|
VkPipelineColorBlendAttachmentState colorBlendAttach{};
|
|
918
1116
|
colorBlendAttach.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
|
|
@@ -941,10 +1139,34 @@ void VulkanBackend::createSkyPipeline() {
|
|
|
941
1139
|
pipelineInfo.pDepthStencilState = &depthStencil;
|
|
942
1140
|
pipelineInfo.pColorBlendState = &colorBlend;
|
|
943
1141
|
pipelineInfo.pDynamicState = &dynamicState;
|
|
944
|
-
pipelineInfo.layout =
|
|
945
|
-
pipelineInfo.renderPass =
|
|
1142
|
+
pipelineInfo.layout = layout;
|
|
1143
|
+
pipelineInfo.renderPass = renderPass;
|
|
946
1144
|
pipelineInfo.subpass = 0;
|
|
947
|
-
|
|
1145
|
+
|
|
1146
|
+
VkPipeline result = VK_NULL_HANDLE;
|
|
1147
|
+
VK_CHECK(vkCreateGraphicsPipelines(device, cache, 1, &pipelineInfo, nullptr, &result));
|
|
1148
|
+
return result;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
} // namespace
|
|
1152
|
+
|
|
1153
|
+
void VulkanBackend::createSkyPipeline() {
|
|
1154
|
+
VkShaderModule vertModule = createShaderModule(device_, spv_sky_vert, spv_sky_vert_size);
|
|
1155
|
+
VkShaderModule fragModule = createShaderModule(device_, spv_sky_frag, spv_sky_frag_size);
|
|
1156
|
+
|
|
1157
|
+
PipelineBuildSpec spec{};
|
|
1158
|
+
spec.vert = vertModule;
|
|
1159
|
+
spec.frag = fragModule;
|
|
1160
|
+
spec.bindingCount = 0;
|
|
1161
|
+
spec.bindings = nullptr;
|
|
1162
|
+
spec.attributeCount = 0;
|
|
1163
|
+
spec.attributes = nullptr;
|
|
1164
|
+
spec.depthTest = false;
|
|
1165
|
+
spec.depthWrite = false;
|
|
1166
|
+
spec.cull = VK_CULL_MODE_NONE;
|
|
1167
|
+
spec.samples = sampleCount_;
|
|
1168
|
+
skyPipeline_ = buildPipeline(device_, skyPipelineLayout_, renderPass_,
|
|
1169
|
+
pipelineCache_, spec);
|
|
948
1170
|
|
|
949
1171
|
vkDestroyShaderModule(device_, vertModule, nullptr);
|
|
950
1172
|
vkDestroyShaderModule(device_, fragModule, nullptr);
|
|
@@ -954,154 +1176,117 @@ void VulkanBackend::createTerrainPipeline() {
|
|
|
954
1176
|
VkShaderModule vertModule = createShaderModule(device_, spv_terrain_vert, spv_terrain_vert_size);
|
|
955
1177
|
VkShaderModule fragModule = createShaderModule(device_, spv_terrain_frag, spv_terrain_frag_size);
|
|
956
1178
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
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;
|
|
1179
|
+
// Vertex bindings: position (vec3) | uv (vec2) | altitude (float).
|
|
1180
|
+
static const VkVertexInputBindingDescription bindings[3] = {
|
|
1181
|
+
{0, 3 * sizeof(float), VK_VERTEX_INPUT_RATE_VERTEX},
|
|
1182
|
+
{1, 2 * sizeof(float), VK_VERTEX_INPUT_RATE_VERTEX},
|
|
1183
|
+
{2, sizeof(float), VK_VERTEX_INPUT_RATE_VERTEX},
|
|
1184
|
+
};
|
|
1185
|
+
static const VkVertexInputAttributeDescription attrs[3] = {
|
|
1186
|
+
{0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0},
|
|
1187
|
+
{1, 1, VK_FORMAT_R32G32_SFLOAT, 0},
|
|
1188
|
+
{2, 2, VK_FORMAT_R32_SFLOAT, 0},
|
|
1189
|
+
};
|
|
1044
1190
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
pipelineInfo.renderPass = renderPass_;
|
|
1059
|
-
pipelineInfo.subpass = 0;
|
|
1060
|
-
VK_CHECK(vkCreateGraphicsPipelines(device_, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &terrainPipeline_));
|
|
1191
|
+
PipelineBuildSpec spec{};
|
|
1192
|
+
spec.vert = vertModule;
|
|
1193
|
+
spec.frag = fragModule;
|
|
1194
|
+
spec.bindingCount = 3;
|
|
1195
|
+
spec.bindings = bindings;
|
|
1196
|
+
spec.attributeCount = 3;
|
|
1197
|
+
spec.attributes = attrs;
|
|
1198
|
+
spec.depthTest = true;
|
|
1199
|
+
spec.depthWrite = true;
|
|
1200
|
+
spec.cull = VK_CULL_MODE_BACK_BIT;
|
|
1201
|
+
spec.samples = sampleCount_;
|
|
1202
|
+
terrainPipeline_ = buildPipeline(device_, terrainPipelineLayout_, renderPass_,
|
|
1203
|
+
pipelineCache_, spec);
|
|
1061
1204
|
|
|
1062
1205
|
vkDestroyShaderModule(device_, vertModule, nullptr);
|
|
1063
1206
|
vkDestroyShaderModule(device_, fragModule, nullptr);
|
|
1064
1207
|
}
|
|
1065
1208
|
|
|
1066
|
-
|
|
1209
|
+
void VulkanBackend::createGraphicsPipelinesAll() {
|
|
1210
|
+
createSkyPipeline();
|
|
1211
|
+
createTerrainPipeline();
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
void VulkanBackend::destroyGraphicsPipelinesAll() {
|
|
1215
|
+
if (terrainPipeline_) vkDestroyPipeline(device_, terrainPipeline_, nullptr);
|
|
1216
|
+
if (skyPipeline_) vkDestroyPipeline(device_, skyPipeline_, nullptr);
|
|
1217
|
+
terrainPipeline_ = skyPipeline_ = VK_NULL_HANDLE;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// ── Memory / buffer helpers ─────────────────────────────────────────────────────
|
|
1067
1221
|
|
|
1068
|
-
uint32_t VulkanBackend::findMemoryType(uint32_t typeFilter,
|
|
1222
|
+
uint32_t VulkanBackend::findMemoryType(uint32_t typeFilter,
|
|
1223
|
+
VkMemoryPropertyFlags props,
|
|
1224
|
+
bool* outFound) {
|
|
1069
1225
|
VkPhysicalDeviceMemoryProperties memProps;
|
|
1070
1226
|
vkGetPhysicalDeviceMemoryProperties(physicalDevice_, &memProps);
|
|
1071
1227
|
for (uint32_t i = 0; i < memProps.memoryTypeCount; ++i) {
|
|
1072
1228
|
if ((typeFilter & (1 << i)) &&
|
|
1073
1229
|
(memProps.memoryTypes[i].propertyFlags & props) == props) {
|
|
1230
|
+
if (outFound) *outFound = true;
|
|
1074
1231
|
return i;
|
|
1075
1232
|
}
|
|
1076
1233
|
}
|
|
1077
|
-
|
|
1234
|
+
if (outFound) {
|
|
1235
|
+
*outFound = false;
|
|
1236
|
+
} else {
|
|
1237
|
+
LOGE("Failed to find suitable memory type (filter=0x%x props=0x%x)",
|
|
1238
|
+
typeFilter, props);
|
|
1239
|
+
}
|
|
1078
1240
|
return 0;
|
|
1079
1241
|
}
|
|
1080
1242
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1243
|
+
bool VulkanBackend::createBuffer(VkDeviceSize size, VkBufferUsageFlags usage,
|
|
1244
|
+
VkMemoryPropertyFlags props,
|
|
1245
|
+
VkBuffer& buffer, VkDeviceMemory& memory) {
|
|
1084
1246
|
VkBufferCreateInfo bufInfo{};
|
|
1085
1247
|
bufInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
|
1086
1248
|
bufInfo.size = size;
|
|
1087
1249
|
bufInfo.usage = usage;
|
|
1088
1250
|
bufInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
|
1089
|
-
|
|
1251
|
+
if (vkCreateBuffer(device_, &bufInfo, nullptr, &buffer) != VK_SUCCESS) {
|
|
1252
|
+
return false;
|
|
1253
|
+
}
|
|
1090
1254
|
|
|
1091
1255
|
VkMemoryRequirements memReqs;
|
|
1092
1256
|
vkGetBufferMemoryRequirements(device_, buffer, &memReqs);
|
|
1093
1257
|
|
|
1258
|
+
bool found = false;
|
|
1259
|
+
uint32_t typeIdx = findMemoryType(memReqs.memoryTypeBits, props, &found);
|
|
1260
|
+
if (!found) {
|
|
1261
|
+
LOGE("createBuffer: no compatible memory type for usage=0x%x props=0x%x",
|
|
1262
|
+
usage, props);
|
|
1263
|
+
vkDestroyBuffer(device_, buffer, nullptr);
|
|
1264
|
+
buffer = VK_NULL_HANDLE;
|
|
1265
|
+
return false;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1094
1268
|
VkMemoryAllocateInfo allocInfo{};
|
|
1095
1269
|
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
|
|
1096
1270
|
allocInfo.allocationSize = memReqs.size;
|
|
1097
|
-
allocInfo.memoryTypeIndex =
|
|
1098
|
-
|
|
1099
|
-
|
|
1271
|
+
allocInfo.memoryTypeIndex = typeIdx;
|
|
1272
|
+
if (vkAllocateMemory(device_, &allocInfo, nullptr, &memory) != VK_SUCCESS) {
|
|
1273
|
+
vkDestroyBuffer(device_, buffer, nullptr);
|
|
1274
|
+
buffer = VK_NULL_HANDLE;
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
if (vkBindBufferMemory(device_, buffer, memory, 0) != VK_SUCCESS) {
|
|
1278
|
+
vkDestroyBuffer(device_, buffer, nullptr);
|
|
1279
|
+
vkFreeMemory(device_, memory, nullptr);
|
|
1280
|
+
buffer = VK_NULL_HANDLE;
|
|
1281
|
+
memory = VK_NULL_HANDLE;
|
|
1282
|
+
return false;
|
|
1283
|
+
}
|
|
1284
|
+
return true;
|
|
1100
1285
|
}
|
|
1101
1286
|
|
|
1102
1287
|
void VulkanBackend::ensureBuffer(VkBuffer& buffer, VkDeviceMemory& memory,
|
|
1103
|
-
|
|
1104
|
-
|
|
1288
|
+
size_t& capacity, size_t needed,
|
|
1289
|
+
const void* data, VkBufferUsageFlags usage) {
|
|
1105
1290
|
if (needed == 0) return;
|
|
1106
1291
|
if (needed > capacity) {
|
|
1107
1292
|
if (buffer) vkDestroyBuffer(device_, buffer, nullptr);
|
|
@@ -1113,8 +1298,8 @@ void VulkanBackend::ensureBuffer(VkBuffer& buffer, VkDeviceMemory& memory,
|
|
|
1113
1298
|
buffer, memory);
|
|
1114
1299
|
capacity = newCap;
|
|
1115
1300
|
}
|
|
1116
|
-
void* mapped;
|
|
1117
|
-
vkMapMemory(device_, memory, 0, needed, 0, &mapped);
|
|
1301
|
+
void* mapped = nullptr;
|
|
1302
|
+
if (vkMapMemory(device_, memory, 0, needed, 0, &mapped) != VK_SUCCESS) return;
|
|
1118
1303
|
memcpy(mapped, data, needed);
|
|
1119
1304
|
vkUnmapMemory(device_, memory);
|
|
1120
1305
|
}
|
|
@@ -1126,17 +1311,25 @@ VkCommandBuffer VulkanBackend::beginSingleTimeCommands() {
|
|
|
1126
1311
|
allocInfo.commandPool = commandPool_;
|
|
1127
1312
|
allocInfo.commandBufferCount = 1;
|
|
1128
1313
|
|
|
1129
|
-
VkCommandBuffer cmd;
|
|
1130
|
-
vkAllocateCommandBuffers(device_, &allocInfo, &cmd)
|
|
1314
|
+
VkCommandBuffer cmd = VK_NULL_HANDLE;
|
|
1315
|
+
if (vkAllocateCommandBuffers(device_, &allocInfo, &cmd) != VK_SUCCESS) {
|
|
1316
|
+
LOGE("beginSingleTimeCommands: vkAllocateCommandBuffers failed");
|
|
1317
|
+
return VK_NULL_HANDLE;
|
|
1318
|
+
}
|
|
1131
1319
|
|
|
1132
1320
|
VkCommandBufferBeginInfo beginInfo{};
|
|
1133
1321
|
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
|
1134
1322
|
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
|
1135
|
-
vkBeginCommandBuffer(cmd, &beginInfo)
|
|
1323
|
+
if (vkBeginCommandBuffer(cmd, &beginInfo) != VK_SUCCESS) {
|
|
1324
|
+
LOGE("beginSingleTimeCommands: vkBeginCommandBuffer failed");
|
|
1325
|
+
vkFreeCommandBuffers(device_, commandPool_, 1, &cmd);
|
|
1326
|
+
return VK_NULL_HANDLE;
|
|
1327
|
+
}
|
|
1136
1328
|
return cmd;
|
|
1137
1329
|
}
|
|
1138
1330
|
|
|
1139
|
-
|
|
1331
|
+
bool VulkanBackend::endSingleTimeCommands(VkCommandBuffer cmd) {
|
|
1332
|
+
if (cmd == VK_NULL_HANDLE) return false;
|
|
1140
1333
|
vkEndCommandBuffer(cmd);
|
|
1141
1334
|
|
|
1142
1335
|
VkSubmitInfo submitInfo{};
|
|
@@ -1144,175 +1337,409 @@ void VulkanBackend::endSingleTimeCommands(VkCommandBuffer cmd) {
|
|
|
1144
1337
|
submitInfo.commandBufferCount = 1;
|
|
1145
1338
|
submitInfo.pCommandBuffers = &cmd;
|
|
1146
1339
|
|
|
1147
|
-
vkQueueSubmit(graphicsQueue_, 1, &submitInfo, VK_NULL_HANDLE);
|
|
1340
|
+
VkResult r = vkQueueSubmit(graphicsQueue_, 1, &submitInfo, VK_NULL_HANDLE);
|
|
1341
|
+
if (r != VK_SUCCESS) {
|
|
1342
|
+
LOGE("endSingleTimeCommands: vkQueueSubmit failed: %d", r);
|
|
1343
|
+
vkFreeCommandBuffers(device_, commandPool_, 1, &cmd);
|
|
1344
|
+
return false;
|
|
1345
|
+
}
|
|
1346
|
+
// We still wait here because this path is reserved for init / fallback. The
|
|
1347
|
+
// hot per-tile upload path uses the deferred staging ring instead.
|
|
1148
1348
|
vkQueueWaitIdle(graphicsQueue_);
|
|
1149
1349
|
vkFreeCommandBuffers(device_, commandPool_, 1, &cmd);
|
|
1350
|
+
return true;
|
|
1150
1351
|
}
|
|
1151
1352
|
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1353
|
+
// ── Mipmap generation ──────────────────────────────────────────────────────────
|
|
1354
|
+
|
|
1355
|
+
void VulkanBackend::recordGenerateMipmaps(VkCommandBuffer cmd, VkImage image,
|
|
1356
|
+
uint32_t width, uint32_t height,
|
|
1357
|
+
uint32_t mipLevels) {
|
|
1358
|
+
// Pre-condition: every level is in TRANSFER_DST_OPTIMAL.
|
|
1359
|
+
// Post-condition: every level is in SHADER_READ_ONLY_OPTIMAL.
|
|
1360
|
+
int32_t mipW = static_cast<int32_t>(width);
|
|
1361
|
+
int32_t mipH = static_cast<int32_t>(height);
|
|
1362
|
+
|
|
1363
|
+
for (uint32_t i = 1; i < mipLevels; ++i) {
|
|
1364
|
+
// Transition source mip (i-1) to TRANSFER_SRC.
|
|
1365
|
+
VkImageMemoryBarrier toSrc{};
|
|
1366
|
+
toSrc.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
|
1367
|
+
toSrc.image = image;
|
|
1368
|
+
toSrc.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
1369
|
+
toSrc.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
|
1370
|
+
toSrc.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
|
1371
|
+
toSrc.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
|
1372
|
+
toSrc.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
1373
|
+
toSrc.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
1374
|
+
toSrc.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, i - 1, 1, 0, 1};
|
|
1375
|
+
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
|
1376
|
+
VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
|
|
1377
|
+
0, nullptr, 0, nullptr, 1, &toSrc);
|
|
1378
|
+
|
|
1379
|
+
VkImageBlit blit{};
|
|
1380
|
+
blit.srcOffsets[0] = {0, 0, 0};
|
|
1381
|
+
blit.srcOffsets[1] = {mipW, mipH, 1};
|
|
1382
|
+
blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
1383
|
+
blit.srcSubresource.mipLevel = i - 1;
|
|
1384
|
+
blit.srcSubresource.baseArrayLayer = 0;
|
|
1385
|
+
blit.srcSubresource.layerCount = 1;
|
|
1386
|
+
blit.dstOffsets[0] = {0, 0, 0};
|
|
1387
|
+
blit.dstOffsets[1] = {std::max(1, mipW / 2), std::max(1, mipH / 2), 1};
|
|
1388
|
+
blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
1389
|
+
blit.dstSubresource.mipLevel = i;
|
|
1390
|
+
blit.dstSubresource.baseArrayLayer = 0;
|
|
1391
|
+
blit.dstSubresource.layerCount = 1;
|
|
1392
|
+
vkCmdBlitImage(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
|
1393
|
+
image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
1394
|
+
1, &blit, VK_FILTER_LINEAR);
|
|
1395
|
+
|
|
1396
|
+
// Source mip (i-1) → SHADER_READ.
|
|
1397
|
+
VkImageMemoryBarrier toShader{};
|
|
1398
|
+
toShader.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
|
1399
|
+
toShader.image = image;
|
|
1400
|
+
toShader.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
|
1401
|
+
toShader.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
|
1402
|
+
toShader.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
|
1403
|
+
toShader.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
1404
|
+
toShader.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
1405
|
+
toShader.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
1406
|
+
toShader.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, i - 1, 1, 0, 1};
|
|
1407
|
+
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
|
1408
|
+
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0,
|
|
1409
|
+
0, nullptr, 0, nullptr, 1, &toShader);
|
|
1410
|
+
|
|
1411
|
+
if (mipW > 1) mipW /= 2;
|
|
1412
|
+
if (mipH > 1) mipH /= 2;
|
|
1413
|
+
}
|
|
1196
1414
|
|
|
1197
|
-
//
|
|
1415
|
+
// Last mip → SHADER_READ.
|
|
1416
|
+
VkImageMemoryBarrier last{};
|
|
1417
|
+
last.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
|
1418
|
+
last.image = image;
|
|
1419
|
+
last.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
1420
|
+
last.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
|
1421
|
+
last.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
|
1422
|
+
last.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
1423
|
+
last.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
1424
|
+
last.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
1425
|
+
last.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, mipLevels - 1, 1, 0, 1};
|
|
1426
|
+
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
|
1427
|
+
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0,
|
|
1428
|
+
0, nullptr, 0, nullptr, 1, &last);
|
|
1429
|
+
}
|
|
1198
1430
|
|
|
1199
|
-
|
|
1200
|
-
const uint8_t white[4] = {255, 255, 255, 255};
|
|
1431
|
+
// ── Persistent staging ring ────────────────────────────────────────────────────
|
|
1201
1432
|
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1433
|
+
void VulkanBackend::initStagingRing(VkDeviceSize bytes) {
|
|
1434
|
+
if (!createBuffer(bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
|
|
1435
|
+
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
|
|
1436
|
+
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
|
1437
|
+
stagingBuffer_, stagingMemory_)) {
|
|
1438
|
+
LOGE("Staging ring allocation failed (size=%llu) — falling back to "
|
|
1439
|
+
"synchronous per-tile upload",
|
|
1440
|
+
static_cast<unsigned long long>(bytes));
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
void* mapped = nullptr;
|
|
1444
|
+
if (vkMapMemory(device_, stagingMemory_, 0, bytes, 0, &mapped) != VK_SUCCESS) {
|
|
1445
|
+
LOGE("Staging ring vkMapMemory failed");
|
|
1446
|
+
vkDestroyBuffer(device_, stagingBuffer_, nullptr);
|
|
1447
|
+
vkFreeMemory(device_, stagingMemory_, nullptr);
|
|
1448
|
+
stagingBuffer_ = VK_NULL_HANDLE;
|
|
1449
|
+
stagingMemory_ = VK_NULL_HANDLE;
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
stagingMapped_ = static_cast<uint8_t*>(mapped);
|
|
1453
|
+
stagingHead_ = 0;
|
|
1454
|
+
}
|
|
1214
1455
|
|
|
1215
|
-
|
|
1216
|
-
|
|
1456
|
+
VkDeviceSize VulkanBackend::allocateStagingSlice(VkDeviceSize size) {
|
|
1457
|
+
if (!stagingMapped_ || size == 0 || size > kStagingSize) return SIZE_MAX;
|
|
1458
|
+
|
|
1459
|
+
VkDeviceSize aligned = (size + kStagingAlignment - 1) & ~(kStagingAlignment - 1);
|
|
1460
|
+
VkDeviceSize off;
|
|
1461
|
+
if (stagingHead_ + aligned > kStagingSize) {
|
|
1462
|
+
// Wrap to the start of the ring. Anything still in flight from before the
|
|
1463
|
+
// wrap will keep its previous offset alive until its upload fence
|
|
1464
|
+
// signalled in beginFrame.
|
|
1465
|
+
off = 0;
|
|
1466
|
+
stagingHead_ = aligned;
|
|
1467
|
+
} else {
|
|
1468
|
+
off = stagingHead_;
|
|
1469
|
+
stagingHead_ += aligned;
|
|
1470
|
+
}
|
|
1471
|
+
return off;
|
|
1472
|
+
}
|
|
1217
1473
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1474
|
+
void VulkanBackend::flushPendingUploads() {
|
|
1475
|
+
std::vector<PendingUpload> uploads;
|
|
1476
|
+
{
|
|
1477
|
+
std::lock_guard<std::mutex> lk(stagingMutex_);
|
|
1478
|
+
if (pendingUploadsCurrent_.empty()) {
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
uploads.swap(pendingUploadsCurrent_);
|
|
1482
|
+
}
|
|
1225
1483
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1484
|
+
VkCommandBuffer cmd = uploadCommandBuffers_[frameIndex_];
|
|
1485
|
+
if (cmd == VK_NULL_HANDLE) {
|
|
1486
|
+
LOGE("flushPendingUploads: upload command buffer is NULL!");
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
vkResetCommandBuffer(cmd, 0);
|
|
1491
|
+
|
|
1492
|
+
VkCommandBufferBeginInfo bi{};
|
|
1493
|
+
bi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
|
1494
|
+
bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
|
1495
|
+
vkBeginCommandBuffer(cmd, &bi);
|
|
1496
|
+
|
|
1497
|
+
// Phase 1: batched UNDEFINED → TRANSFER_DST_OPTIMAL barriers.
|
|
1498
|
+
std::vector<VkImageMemoryBarrier> toTransfer;
|
|
1499
|
+
toTransfer.reserve(uploads.size());
|
|
1500
|
+
for (const auto& u : uploads) {
|
|
1501
|
+
// Defensive: mipLevels should be 1-15 for any reasonable texture.
|
|
1502
|
+
uint32_t safeMipLevels = u.tex->mipLevels;
|
|
1503
|
+
if (safeMipLevels == 0 || safeMipLevels > 16) {
|
|
1504
|
+
LOGE("flushPendingUploads: corrupt mipLevels=%u for tex=%p, clamping to 1",
|
|
1505
|
+
safeMipLevels, (void*)u.tex);
|
|
1506
|
+
safeMipLevels = 1;
|
|
1507
|
+
u.tex->mipLevels = 1;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
VkImageMemoryBarrier b{};
|
|
1511
|
+
b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
|
1512
|
+
b.srcAccessMask = 0;
|
|
1513
|
+
b.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
1514
|
+
b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
1515
|
+
b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
|
1516
|
+
b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
1517
|
+
b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
1518
|
+
b.image = u.tex->image;
|
|
1519
|
+
b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, safeMipLevels, 0, 1};
|
|
1520
|
+
toTransfer.push_back(b);
|
|
1521
|
+
}
|
|
1522
|
+
if (!toTransfer.empty()) {
|
|
1523
|
+
vkCmdPipelineBarrier(cmd,
|
|
1524
|
+
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
|
1525
|
+
0, 0, nullptr, 0, nullptr,
|
|
1526
|
+
static_cast<uint32_t>(toTransfer.size()), toTransfer.data());
|
|
1527
|
+
}
|
|
1231
1528
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1529
|
+
// Phase 2: per-image vkCmdCopyBufferToImage from the staging ring.
|
|
1530
|
+
if (stagingBuffer_ == VK_NULL_HANDLE) {
|
|
1531
|
+
LOGE("flushPendingUploads: stagingBuffer_ is NULL! Aborting upload");
|
|
1532
|
+
vkEndCommandBuffer(cmd);
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
for (const auto& u : uploads) {
|
|
1536
|
+
VkBufferImageCopy region{};
|
|
1537
|
+
region.bufferOffset = u.stagingOffset;
|
|
1538
|
+
region.bufferRowLength = 0;
|
|
1539
|
+
region.bufferImageHeight = 0;
|
|
1540
|
+
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
|
1541
|
+
region.imageExtent = {u.width, u.height, 1};
|
|
1542
|
+
vkCmdCopyBufferToImage(cmd, stagingBuffer_, u.tex->image,
|
|
1543
|
+
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion);
|
|
1544
|
+
}
|
|
1236
1545
|
|
|
1237
|
-
|
|
1546
|
+
// Phase 3: per-image mipmap chain (only for textures that asked for it) +
|
|
1547
|
+
// a batched TRANSFER_DST → SHADER_READ for the rest.
|
|
1548
|
+
std::vector<VkImageMemoryBarrier> toShader;
|
|
1549
|
+
toShader.reserve(uploads.size());
|
|
1550
|
+
for (const auto& u : uploads) {
|
|
1551
|
+
if (u.tex->mipLevels > 1) {
|
|
1552
|
+
recordGenerateMipmaps(cmd, u.tex->image, u.width, u.height,
|
|
1553
|
+
u.tex->mipLevels);
|
|
1554
|
+
} else {
|
|
1555
|
+
VkImageMemoryBarrier b{};
|
|
1556
|
+
b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
|
1557
|
+
b.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
1558
|
+
b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
|
1559
|
+
b.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
|
1560
|
+
b.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
1561
|
+
b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
1562
|
+
b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
1563
|
+
b.image = u.tex->image;
|
|
1564
|
+
b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
|
1565
|
+
toShader.push_back(b);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
if (!toShader.empty()) {
|
|
1569
|
+
vkCmdPipelineBarrier(cmd,
|
|
1570
|
+
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
|
1571
|
+
0, 0, nullptr, 0, nullptr,
|
|
1572
|
+
static_cast<uint32_t>(toShader.size()), toShader.data());
|
|
1573
|
+
}
|
|
1238
1574
|
|
|
1239
|
-
|
|
1240
|
-
vkFreeMemory(device_, stagingMem, nullptr);
|
|
1575
|
+
vkEndCommandBuffer(cmd);
|
|
1241
1576
|
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1577
|
+
// SYNCHRONOUS submit and wait - simpler and more reliable than fence-based
|
|
1578
|
+
// batching. The performance hit is minimal since uploads are infrequent.
|
|
1579
|
+
VkSubmitInfo si{};
|
|
1580
|
+
si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
|
1581
|
+
si.commandBufferCount = 1;
|
|
1582
|
+
si.pCommandBuffers = &cmd;
|
|
1583
|
+
|
|
1584
|
+
VkResult submitResult = vkQueueSubmit(graphicsQueue_, 1, &si, VK_NULL_HANDLE);
|
|
1585
|
+
if (submitResult != VK_SUCCESS) {
|
|
1586
|
+
LOGE("flushPendingUploads: vkQueueSubmit failed: %d", submitResult);
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
vkQueueWaitIdle(graphicsQueue_);
|
|
1249
1591
|
|
|
1250
|
-
|
|
1251
|
-
|
|
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));
|
|
1592
|
+
// Mark each texture as visible to subsequent draws (no longer pending).
|
|
1593
|
+
for (auto& u : uploads) u.tex->pendingUpload = false;
|
|
1258
1594
|
}
|
|
1259
1595
|
|
|
1260
|
-
// ──
|
|
1596
|
+
// ── Generic image creator (P2-pipelines-dedupe) ─────────────────────────────
|
|
1261
1597
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1598
|
+
VulkanTexture* VulkanBackend::createImageFromPixels(const uint8_t* pixels,
|
|
1599
|
+
int32_t width, int32_t height,
|
|
1600
|
+
VkDescriptorPool pool,
|
|
1601
|
+
VkSampleCountFlagBits samples,
|
|
1602
|
+
bool wantMipmaps) {
|
|
1603
|
+
if (!device_ || !pixels || width <= 0 || height <= 0 || pool == VK_NULL_HANDLE)
|
|
1604
|
+
return nullptr;
|
|
1264
1605
|
|
|
1265
1606
|
auto* tex = new VulkanTexture();
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
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);
|
|
1607
|
+
tex->width = static_cast<uint32_t>(width);
|
|
1608
|
+
tex->height = static_cast<uint32_t>(height);
|
|
1609
|
+
tex->mipLevels = wantMipmaps
|
|
1610
|
+
? static_cast<uint32_t>(std::floor(std::log2(std::max(width, height)))) + 1u
|
|
1611
|
+
: 1u;
|
|
1278
1612
|
|
|
1279
1613
|
VkImageCreateInfo imgInfo{};
|
|
1280
1614
|
imgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
|
1281
1615
|
imgInfo.imageType = VK_IMAGE_TYPE_2D;
|
|
1282
1616
|
imgInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
|
|
1283
|
-
imgInfo.extent = {
|
|
1284
|
-
imgInfo.mipLevels =
|
|
1617
|
+
imgInfo.extent = {tex->width, tex->height, 1};
|
|
1618
|
+
imgInfo.mipLevels = tex->mipLevels;
|
|
1285
1619
|
imgInfo.arrayLayers = 1;
|
|
1286
|
-
imgInfo.samples =
|
|
1620
|
+
imgInfo.samples = samples;
|
|
1287
1621
|
imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
|
1288
|
-
imgInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT |
|
|
1622
|
+
imgInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT |
|
|
1623
|
+
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | // mipmap blit src
|
|
1624
|
+
VK_IMAGE_USAGE_SAMPLED_BIT;
|
|
1289
1625
|
imgInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
1290
|
-
|
|
1626
|
+
if (vkCreateImage(device_, &imgInfo, nullptr, &tex->image) != VK_SUCCESS) {
|
|
1627
|
+
delete tex;
|
|
1628
|
+
return nullptr;
|
|
1629
|
+
}
|
|
1291
1630
|
|
|
1292
1631
|
VkMemoryRequirements memReqs;
|
|
1293
1632
|
vkGetImageMemoryRequirements(device_, tex->image, &memReqs);
|
|
1294
1633
|
|
|
1634
|
+
bool typeFound = false;
|
|
1635
|
+
uint32_t typeIdx = findMemoryType(memReqs.memoryTypeBits,
|
|
1636
|
+
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
|
|
1637
|
+
&typeFound);
|
|
1638
|
+
if (!typeFound) {
|
|
1639
|
+
LOGE("createImageFromPixels: no DEVICE_LOCAL memory type available");
|
|
1640
|
+
vkDestroyImage(device_, tex->image, nullptr);
|
|
1641
|
+
delete tex;
|
|
1642
|
+
return nullptr;
|
|
1643
|
+
}
|
|
1295
1644
|
VkMemoryAllocateInfo allocInfo{};
|
|
1296
1645
|
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
|
|
1297
1646
|
allocInfo.allocationSize = memReqs.size;
|
|
1298
|
-
allocInfo.memoryTypeIndex =
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1647
|
+
allocInfo.memoryTypeIndex = typeIdx;
|
|
1648
|
+
if (vkAllocateMemory(device_, &allocInfo, nullptr, &tex->memory) != VK_SUCCESS) {
|
|
1649
|
+
vkDestroyImage(device_, tex->image, nullptr);
|
|
1650
|
+
delete tex;
|
|
1651
|
+
return nullptr;
|
|
1652
|
+
}
|
|
1653
|
+
vkBindImageMemory(device_, tex->image, tex->memory, 0);
|
|
1654
|
+
|
|
1655
|
+
// Try the deferred staging path first; fall back to synchronous if the ring
|
|
1656
|
+
// is full or we are still pre-init (no command buffers yet).
|
|
1657
|
+
const VkDeviceSize imageBytes =
|
|
1658
|
+
static_cast<VkDeviceSize>(width) * height * 4;
|
|
1659
|
+
bool deferred = false;
|
|
1660
|
+
if (stagingMapped_ != nullptr && commandPool_ != VK_NULL_HANDLE) {
|
|
1661
|
+
std::lock_guard<std::mutex> lk(stagingMutex_);
|
|
1662
|
+
VkDeviceSize off = allocateStagingSlice(imageBytes);
|
|
1663
|
+
if (off != static_cast<VkDeviceSize>(SIZE_MAX) && off + imageBytes <= kStagingSize) {
|
|
1664
|
+
memcpy(stagingMapped_ + off, pixels, static_cast<size_t>(imageBytes));
|
|
1665
|
+
tex->pendingUpload = true;
|
|
1666
|
+
pendingUploadsCurrent_.push_back(
|
|
1667
|
+
{tex, off, tex->width, tex->height, totalFrameCount_});
|
|
1668
|
+
deferred = true;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1305
1671
|
|
|
1306
|
-
|
|
1307
|
-
|
|
1672
|
+
if (!deferred) {
|
|
1673
|
+
// Synchronous fallback (init-time or staging-ring exhausted).
|
|
1674
|
+
VkBuffer staging;
|
|
1675
|
+
VkDeviceMemory stagingMem;
|
|
1676
|
+
if (!createBuffer(imageBytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
|
|
1677
|
+
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
|
|
1678
|
+
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
|
1679
|
+
staging, stagingMem)) {
|
|
1680
|
+
vkDestroyImage(device_, tex->image, nullptr);
|
|
1681
|
+
vkFreeMemory(device_, tex->memory, nullptr);
|
|
1682
|
+
delete tex;
|
|
1683
|
+
return nullptr;
|
|
1684
|
+
}
|
|
1685
|
+
void* mapped = nullptr;
|
|
1686
|
+
vkMapMemory(device_, stagingMem, 0, imageBytes, 0, &mapped);
|
|
1687
|
+
memcpy(mapped, pixels, static_cast<size_t>(imageBytes));
|
|
1688
|
+
vkUnmapMemory(device_, stagingMem);
|
|
1689
|
+
|
|
1690
|
+
VkCommandBuffer cmd = beginSingleTimeCommands();
|
|
1691
|
+
if (cmd != VK_NULL_HANDLE) {
|
|
1692
|
+
VkImageMemoryBarrier toTransfer{};
|
|
1693
|
+
toTransfer.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
|
1694
|
+
toTransfer.srcAccessMask = 0;
|
|
1695
|
+
toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
1696
|
+
toTransfer.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
1697
|
+
toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
|
1698
|
+
toTransfer.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
1699
|
+
toTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
1700
|
+
toTransfer.image = tex->image;
|
|
1701
|
+
toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, tex->mipLevels, 0, 1};
|
|
1702
|
+
vkCmdPipelineBarrier(cmd,
|
|
1703
|
+
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
|
1704
|
+
0, 0, nullptr, 0, nullptr, 1, &toTransfer);
|
|
1705
|
+
|
|
1706
|
+
VkBufferImageCopy region{};
|
|
1707
|
+
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
|
1708
|
+
region.imageExtent = {tex->width, tex->height, 1};
|
|
1709
|
+
vkCmdCopyBufferToImage(cmd, staging, tex->image,
|
|
1710
|
+
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion);
|
|
1711
|
+
|
|
1712
|
+
if (tex->mipLevels > 1) {
|
|
1713
|
+
recordGenerateMipmaps(cmd, tex->image, tex->width, tex->height,
|
|
1714
|
+
tex->mipLevels);
|
|
1715
|
+
} else {
|
|
1716
|
+
VkImageMemoryBarrier toShader{};
|
|
1717
|
+
toShader.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
|
1718
|
+
toShader.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
1719
|
+
toShader.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
|
1720
|
+
toShader.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
|
1721
|
+
toShader.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
1722
|
+
toShader.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
1723
|
+
toShader.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
1724
|
+
toShader.image = tex->image;
|
|
1725
|
+
toShader.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
|
1726
|
+
vkCmdPipelineBarrier(cmd,
|
|
1727
|
+
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
|
1728
|
+
0, 0, nullptr, 0, nullptr, 1, &toShader);
|
|
1729
|
+
}
|
|
1730
|
+
endSingleTimeCommands(cmd);
|
|
1731
|
+
}
|
|
1732
|
+
vkDestroyBuffer(device_, staging, nullptr);
|
|
1733
|
+
vkFreeMemory(device_, stagingMem, nullptr);
|
|
1734
|
+
}
|
|
1308
1735
|
|
|
1309
1736
|
VkImageViewCreateInfo viewInfo{};
|
|
1310
1737
|
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
|
1311
1738
|
viewInfo.image = tex->image;
|
|
1312
1739
|
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
|
1313
1740
|
viewInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
|
|
1314
|
-
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0,
|
|
1315
|
-
|
|
1741
|
+
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, tex->mipLevels, 0, 1};
|
|
1742
|
+
vkCreateImageView(device_, &viewInfo, nullptr, &tex->imageView);
|
|
1316
1743
|
|
|
1317
1744
|
VkSamplerCreateInfo samplerInfo{};
|
|
1318
1745
|
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
|
@@ -1321,12 +1748,19 @@ void* VulkanBackend::createRasterTexture(const uint8_t* pixels, int32_t width, i
|
|
|
1321
1748
|
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
1322
1749
|
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
1323
1750
|
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
1324
|
-
|
|
1751
|
+
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
|
|
1752
|
+
samplerInfo.minLod = 0.0f;
|
|
1753
|
+
samplerInfo.maxLod = static_cast<float>(tex->mipLevels);
|
|
1754
|
+
if (supportsAnisotropy_) {
|
|
1755
|
+
samplerInfo.anisotropyEnable = VK_TRUE;
|
|
1756
|
+
samplerInfo.maxAnisotropy = std::min(8.0f, maxAnisotropy_);
|
|
1757
|
+
}
|
|
1758
|
+
vkCreateSampler(device_, &samplerInfo, nullptr, &tex->sampler);
|
|
1325
1759
|
|
|
1326
|
-
// Allocate a descriptor set for this texture
|
|
1760
|
+
// Allocate a descriptor set for this texture.
|
|
1327
1761
|
VkDescriptorSetAllocateInfo dsAllocInfo{};
|
|
1328
1762
|
dsAllocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
|
1329
|
-
dsAllocInfo.descriptorPool =
|
|
1763
|
+
dsAllocInfo.descriptorPool = pool;
|
|
1330
1764
|
dsAllocInfo.descriptorSetCount = 1;
|
|
1331
1765
|
dsAllocInfo.pSetLayouts = &terrainTexLayout_;
|
|
1332
1766
|
VkResult dsResult = vkAllocateDescriptorSets(device_, &dsAllocInfo, &tex->descriptorSet);
|
|
@@ -1348,115 +1782,31 @@ void* VulkanBackend::createRasterTexture(const uint8_t* pixels, int32_t width, i
|
|
|
1348
1782
|
texWrite.pImageInfo = &dsImgInfo;
|
|
1349
1783
|
vkUpdateDescriptorSets(device_, 1, &texWrite, 0, nullptr);
|
|
1350
1784
|
}
|
|
1351
|
-
|
|
1352
1785
|
return tex;
|
|
1353
1786
|
}
|
|
1354
1787
|
|
|
1788
|
+
// ── Public texture creators ─────────────────────────────────────────────────────
|
|
1789
|
+
|
|
1790
|
+
void* VulkanBackend::createRasterTexture(const uint8_t* pixels, int32_t width, int32_t height) {
|
|
1791
|
+
return createImageFromPixels(pixels, width, height,
|
|
1792
|
+
descriptorPool_, VK_SAMPLE_COUNT_1_BIT,
|
|
1793
|
+
/*wantMipmaps=*/true);
|
|
1794
|
+
}
|
|
1355
1795
|
void VulkanBackend::freeRasterTexture(void* texPtr) {
|
|
1356
1796
|
if (!texPtr || !device_) return;
|
|
1357
1797
|
auto* tex = static_cast<VulkanTexture*>(texPtr);
|
|
1358
|
-
// Enqueue for deferred deletion — pool = descriptorPool_ (set 1 imagery pool).
|
|
1359
1798
|
std::lock_guard<std::mutex> lk(pendingDeletesMutex_);
|
|
1360
1799
|
pendingDeletes_.push_back({tex, totalFrameCount_, descriptorPool_});
|
|
1361
1800
|
}
|
|
1362
|
-
|
|
1363
1801
|
void* VulkanBackend::createWaterMaskTexture(const uint8_t* pixels, int32_t width, int32_t height) {
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
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;
|
|
1802
|
+
// Water mask is downsampled hi-frequency data; mipmaps add real value here too.
|
|
1803
|
+
return createImageFromPixels(pixels, width, height,
|
|
1804
|
+
waterMaskPool_, VK_SAMPLE_COUNT_1_BIT,
|
|
1805
|
+
/*wantMipmaps=*/true);
|
|
1454
1806
|
}
|
|
1455
|
-
|
|
1456
1807
|
void VulkanBackend::freeWaterMaskTexture(void* texPtr) {
|
|
1457
1808
|
if (!texPtr || !device_) return;
|
|
1458
1809
|
auto* tex = static_cast<VulkanTexture*>(texPtr);
|
|
1459
|
-
// Enqueue for deferred deletion — pool = waterMaskPool_ (set 2 pool).
|
|
1460
1810
|
std::lock_guard<std::mutex> lk(pendingDeletesMutex_);
|
|
1461
1811
|
pendingDeletes_.push_back({tex, totalFrameCount_, waterMaskPool_});
|
|
1462
1812
|
}
|
|
@@ -1475,8 +1825,6 @@ void VulkanBackend::destroyTexture(VulkanTexture* tex, VkDescriptorPool pool) {
|
|
|
1475
1825
|
|
|
1476
1826
|
void VulkanBackend::flushPendingDeletes() {
|
|
1477
1827
|
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
1828
|
while (!pendingDeletes_.empty()) {
|
|
1481
1829
|
const auto& front = pendingDeletes_.front();
|
|
1482
1830
|
if (totalFrameCount_ < front.frameIndex + static_cast<uint64_t>(kMaxFramesInFlight))
|
|
@@ -1486,9 +1834,11 @@ void VulkanBackend::flushPendingDeletes() {
|
|
|
1486
1834
|
}
|
|
1487
1835
|
}
|
|
1488
1836
|
|
|
1489
|
-
void VulkanBackend::setMsaaSampleCount(int
|
|
1490
|
-
// MSAA
|
|
1491
|
-
//
|
|
1837
|
+
void VulkanBackend::setMsaaSampleCount(int sampleCount) {
|
|
1838
|
+
// Defer MSAA changes to the next frame boundary so we don't tear down the
|
|
1839
|
+
// render pass mid-frame. Stash the request; beginFrame applies it before any
|
|
1840
|
+
// GPU work for the frame.
|
|
1841
|
+
pendingMsaaRequest_ = sampleCount;
|
|
1492
1842
|
}
|
|
1493
1843
|
|
|
1494
1844
|
// ── Swapchain recreation ───────────────────────────────────────────────────────
|
|
@@ -1497,6 +1847,11 @@ void VulkanBackend::cleanupSwapchain() {
|
|
|
1497
1847
|
for (auto& fb : framebuffers_) vkDestroyFramebuffer(device_, fb, nullptr);
|
|
1498
1848
|
framebuffers_.clear();
|
|
1499
1849
|
|
|
1850
|
+
if (msaaColorView_) vkDestroyImageView(device_, msaaColorView_, nullptr);
|
|
1851
|
+
if (msaaColorImage_) vkDestroyImage(device_, msaaColorImage_, nullptr);
|
|
1852
|
+
if (msaaColorMemory_) vkFreeMemory(device_, msaaColorMemory_, nullptr);
|
|
1853
|
+
msaaColorView_ = VK_NULL_HANDLE; msaaColorImage_ = VK_NULL_HANDLE; msaaColorMemory_ = VK_NULL_HANDLE;
|
|
1854
|
+
|
|
1500
1855
|
if (depthImageView_) vkDestroyImageView(device_, depthImageView_, nullptr);
|
|
1501
1856
|
if (depthImage_) vkDestroyImage(device_, depthImage_, nullptr);
|
|
1502
1857
|
if (depthMemory_) vkFreeMemory(device_, depthMemory_, nullptr);
|
|
@@ -1514,11 +1869,28 @@ void VulkanBackend::cleanupSwapchain() {
|
|
|
1514
1869
|
}
|
|
1515
1870
|
|
|
1516
1871
|
void VulkanBackend::recreateSwapchain() {
|
|
1517
|
-
|
|
1872
|
+
// P2-vk-recreate: drain only the in-flight frames, not the whole device.
|
|
1873
|
+
if (!inFlightFences_.empty()) {
|
|
1874
|
+
vkWaitForFences(device_, static_cast<uint32_t>(inFlightFences_.size()),
|
|
1875
|
+
inFlightFences_.data(), VK_TRUE, UINT64_MAX);
|
|
1876
|
+
}
|
|
1518
1877
|
cleanupSwapchain();
|
|
1519
1878
|
createSwapchain();
|
|
1520
1879
|
createDepthResources();
|
|
1880
|
+
if (sampleCount_ != VK_SAMPLE_COUNT_1_BIT) createMsaaResources();
|
|
1521
1881
|
createFramebuffers();
|
|
1882
|
+
|
|
1883
|
+
// The per-image semaphore array is keyed on swapchain image index, which
|
|
1884
|
+
// can change with image count. Resize it to match.
|
|
1885
|
+
for (auto& s : renderFinishedSemaphores_) {
|
|
1886
|
+
if (s) vkDestroySemaphore(device_, s, nullptr);
|
|
1887
|
+
}
|
|
1888
|
+
renderFinishedSemaphores_.assign(swapchainImages_.size(), VK_NULL_HANDLE);
|
|
1889
|
+
VkSemaphoreCreateInfo si{}; si.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
|
|
1890
|
+
for (auto& s : renderFinishedSemaphores_) {
|
|
1891
|
+
VK_CHECK(vkCreateSemaphore(device_, &si, nullptr, &s));
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1522
1894
|
needsSwapchainRecreate_ = false;
|
|
1523
1895
|
}
|
|
1524
1896
|
|
|
@@ -1526,6 +1898,56 @@ void VulkanBackend::recreateSwapchain() {
|
|
|
1526
1898
|
|
|
1527
1899
|
void VulkanBackend::beginFrame(const FrameParams& /*params*/) {
|
|
1528
1900
|
if (device_ == VK_NULL_HANDLE) return;
|
|
1901
|
+
|
|
1902
|
+
// On Android, defer swapchain creation until the first frame to ensure the
|
|
1903
|
+
// surface compositor is fully ready. Creating it too early can cause
|
|
1904
|
+
// vkAcquireNextImageKHR to block indefinitely.
|
|
1905
|
+
if (swapchain_ == VK_NULL_HANDLE) {
|
|
1906
|
+
createSwapchain();
|
|
1907
|
+
|
|
1908
|
+
// Create renderFinishedSemaphores now that we know swapchainImages_.size()
|
|
1909
|
+
renderFinishedSemaphores_.resize(swapchainImages_.size());
|
|
1910
|
+
VkSemaphoreCreateInfo semInfo{};
|
|
1911
|
+
semInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
|
|
1912
|
+
for (size_t i = 0; i < renderFinishedSemaphores_.size(); ++i) {
|
|
1913
|
+
VK_CHECK(vkCreateSemaphore(device_, &semInfo, nullptr, &renderFinishedSemaphores_[i]));
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
createRenderPass();
|
|
1917
|
+
createDepthResources();
|
|
1918
|
+
if (sampleCount_ != VK_SAMPLE_COUNT_1_BIT) createMsaaResources();
|
|
1919
|
+
createFramebuffers();
|
|
1920
|
+
createGraphicsPipelinesAll();
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
// Apply pending MSAA change between frames (safe — the previous frame's
|
|
1924
|
+
// command buffer has finished by the time we wait on the fence below).
|
|
1925
|
+
if (pendingMsaaRequest_ != 0) {
|
|
1926
|
+
const VkSampleCountFlagBits target = resolveMsaaSamples(pendingMsaaRequest_);
|
|
1927
|
+
pendingMsaaRequest_ = 0;
|
|
1928
|
+
if (target != sampleCount_) {
|
|
1929
|
+
vkDeviceWaitIdle(device_); // bounded; one-shot when MSAA toggled
|
|
1930
|
+
destroyGraphicsPipelinesAll();
|
|
1931
|
+
if (renderPass_) { vkDestroyRenderPass(device_, renderPass_, nullptr); renderPass_ = VK_NULL_HANDLE; }
|
|
1932
|
+
// Need to free the framebuffers + depth + msaa color since attachments
|
|
1933
|
+
// now have a different sample count.
|
|
1934
|
+
for (auto& fb : framebuffers_) vkDestroyFramebuffer(device_, fb, nullptr);
|
|
1935
|
+
framebuffers_.clear();
|
|
1936
|
+
if (depthImageView_) { vkDestroyImageView(device_, depthImageView_, nullptr); depthImageView_ = VK_NULL_HANDLE; }
|
|
1937
|
+
if (depthImage_) { vkDestroyImage(device_, depthImage_, nullptr); depthImage_ = VK_NULL_HANDLE; }
|
|
1938
|
+
if (depthMemory_) { vkFreeMemory(device_, depthMemory_, nullptr); depthMemory_ = VK_NULL_HANDLE; }
|
|
1939
|
+
if (msaaColorView_) { vkDestroyImageView(device_, msaaColorView_, nullptr); msaaColorView_ = VK_NULL_HANDLE; }
|
|
1940
|
+
if (msaaColorImage_) { vkDestroyImage(device_, msaaColorImage_, nullptr); msaaColorImage_ = VK_NULL_HANDLE; }
|
|
1941
|
+
if (msaaColorMemory_){ vkFreeMemory(device_, msaaColorMemory_, nullptr); msaaColorMemory_ = VK_NULL_HANDLE; }
|
|
1942
|
+
|
|
1943
|
+
sampleCount_ = target;
|
|
1944
|
+
createRenderPass();
|
|
1945
|
+
createDepthResources();
|
|
1946
|
+
if (sampleCount_ != VK_SAMPLE_COUNT_1_BIT) createMsaaResources();
|
|
1947
|
+
createFramebuffers();
|
|
1948
|
+
createGraphicsPipelinesAll();
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1529
1951
|
|
|
1530
1952
|
if (needsSwapchainRecreate_) {
|
|
1531
1953
|
recreateSwapchain();
|
|
@@ -1533,13 +1955,33 @@ void VulkanBackend::beginFrame(const FrameParams& /*params*/) {
|
|
|
1533
1955
|
|
|
1534
1956
|
vkWaitForFences(device_, 1, &inFlightFences_[frameIndex_], VK_TRUE, UINT64_MAX);
|
|
1535
1957
|
|
|
1536
|
-
// GPU is done with frameIndex_ — safe to destroy textures enqueued that long ago.
|
|
1537
1958
|
++totalFrameCount_;
|
|
1538
1959
|
flushPendingDeletes();
|
|
1539
1960
|
|
|
1540
|
-
|
|
1961
|
+
// Reclaim staging slices for uploads that have now finished on the GPU.
|
|
1962
|
+
// (Their submit was made N=kMaxFramesInFlight frames ago against
|
|
1963
|
+
// uploadFences_[frameIndex_]; the wait above is on inFlightFences_, but
|
|
1964
|
+
// since uploads were submitted on the same queue *before* the previous
|
|
1965
|
+
// frame's draw they must have completed too.)
|
|
1966
|
+
{
|
|
1967
|
+
std::lock_guard<std::mutex> lk(stagingMutex_);
|
|
1968
|
+
uploadsInFlight_[frameIndex_].clear();
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
// Submit any uploads queued since last beginFrame.
|
|
1972
|
+
flushPendingUploads();
|
|
1973
|
+
|
|
1974
|
+
// Use a 1-second timeout instead of UINT64_MAX to avoid indefinite hangs.
|
|
1975
|
+
// On a properly functioning swapchain, this returns immediately.
|
|
1976
|
+
VkResult result = vkAcquireNextImageKHR(device_, swapchain_, 1000000000ULL,
|
|
1541
1977
|
imageAvailableSemaphores_[frameIndex_],
|
|
1542
1978
|
VK_NULL_HANDLE, &imageIndex_);
|
|
1979
|
+
|
|
1980
|
+
if (result == VK_TIMEOUT) {
|
|
1981
|
+
LOGE("VulkanBackend::beginFrame: vkAcquireNextImageKHR timed out");
|
|
1982
|
+
frameBegan_ = false;
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1543
1985
|
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
|
|
1544
1986
|
recreateSwapchain();
|
|
1545
1987
|
result = vkAcquireNextImageKHR(device_, swapchain_, UINT64_MAX,
|
|
@@ -1558,9 +2000,10 @@ void VulkanBackend::beginFrame(const FrameParams& /*params*/) {
|
|
|
1558
2000
|
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
|
1559
2001
|
vkBeginCommandBuffer(commandBuffers_[frameIndex_], &beginInfo);
|
|
1560
2002
|
|
|
1561
|
-
std::array<VkClearValue,
|
|
2003
|
+
std::array<VkClearValue, 3> clearValues{};
|
|
1562
2004
|
clearValues[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}};
|
|
1563
2005
|
clearValues[1].depthStencil = {0.0f, 0}; // Reversed-Z: 0 = far
|
|
2006
|
+
clearValues[2].color = {{0.0f, 0.0f, 0.0f, 1.0f}};
|
|
1564
2007
|
|
|
1565
2008
|
VkRenderPassBeginInfo rpBegin{};
|
|
1566
2009
|
rpBegin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
|
|
@@ -1568,15 +2011,13 @@ void VulkanBackend::beginFrame(const FrameParams& /*params*/) {
|
|
|
1568
2011
|
rpBegin.framebuffer = framebuffers_[imageIndex_];
|
|
1569
2012
|
rpBegin.renderArea.offset = {0, 0};
|
|
1570
2013
|
rpBegin.renderArea.extent = swapchainExtent_;
|
|
1571
|
-
rpBegin.clearValueCount =
|
|
2014
|
+
rpBegin.clearValueCount = (sampleCount_ != VK_SAMPLE_COUNT_1_BIT) ? 3u : 2u;
|
|
1572
2015
|
rpBegin.pClearValues = clearValues.data();
|
|
1573
2016
|
|
|
1574
2017
|
vkCmdBeginRenderPass(commandBuffers_[frameIndex_], &rpBegin, VK_SUBPASS_CONTENTS_INLINE);
|
|
1575
2018
|
|
|
1576
2019
|
// Negative viewport height flips Y to match the OpenGL/Metal clip-space
|
|
1577
2020
|
// 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
2021
|
VkViewport viewport{};
|
|
1581
2022
|
viewport.x = 0.0f;
|
|
1582
2023
|
viewport.y = static_cast<float>(swapchainExtent_.height);
|
|
@@ -1624,7 +2065,6 @@ void VulkanBackend::drawScene(const FrameResult& frame) {
|
|
|
1624
2065
|
vkCmdDraw(cmd, 3, 1, 0, 0);
|
|
1625
2066
|
}
|
|
1626
2067
|
|
|
1627
|
-
// ── Terrain pass ──────────────────────────────────────────────────────────
|
|
1628
2068
|
if (frame.draws.empty() || frame.localPositions.empty() || frame.indices.empty() ||
|
|
1629
2069
|
frame.altitudes.empty() || !terrainPipeline_) {
|
|
1630
2070
|
return;
|
|
@@ -1648,7 +2088,6 @@ void VulkanBackend::drawScene(const FrameResult& frame) {
|
|
|
1648
2088
|
|
|
1649
2089
|
if (!vtxBufs_[fi] || !idxBufs_[fi]) return;
|
|
1650
2090
|
|
|
1651
|
-
// Per-frame UBO: just cameraEcef (fragment uses it for lighting view direction).
|
|
1652
2091
|
TerrainUBO terrU{};
|
|
1653
2092
|
terrU.cameraEcef[0] = frame.cameraEcef.x;
|
|
1654
2093
|
terrU.cameraEcef[1] = frame.cameraEcef.y;
|
|
@@ -1658,17 +2097,14 @@ void VulkanBackend::drawScene(const FrameResult& frame) {
|
|
|
1658
2097
|
|
|
1659
2098
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, terrainPipeline_);
|
|
1660
2099
|
|
|
1661
|
-
// Bind vertex buffers: binding 0 = positions, 1 = UVs, 2 = altitudes.
|
|
1662
2100
|
VkBuffer vertexBuffers[] = {vtxBufs_[fi], uvBufs_[fi], altBufs_[fi]};
|
|
1663
2101
|
VkDeviceSize offsets[] = {0, 0, 0};
|
|
1664
2102
|
vkCmdBindVertexBuffers(cmd, 0, 3, vertexBuffers, offsets);
|
|
1665
2103
|
vkCmdBindIndexBuffer(cmd, idxBufs_[fi], 0, VK_INDEX_TYPE_UINT32);
|
|
1666
2104
|
|
|
1667
|
-
// Bind set 0 (UBO) once for the whole terrain pass.
|
|
1668
2105
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, terrainPipelineLayout_,
|
|
1669
2106
|
0, 1, &terrainDescSets_[fi], 0, nullptr);
|
|
1670
2107
|
|
|
1671
|
-
// Bind set 1 (imagery) and set 2 (water mask) — start with fallbacks.
|
|
1672
2108
|
VkDescriptorSet lastTexSet = fallbackTexDescSet_;
|
|
1673
2109
|
VkDescriptorSet lastWaterMaskSet = fallbackWaterMaskDescSet_;
|
|
1674
2110
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, terrainPipelineLayout_,
|
|
@@ -1676,41 +2112,47 @@ void VulkanBackend::drawScene(const FrameResult& frame) {
|
|
|
1676
2112
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, terrainPipelineLayout_,
|
|
1677
2113
|
2, 1, &lastWaterMaskSet, 0, nullptr);
|
|
1678
2114
|
|
|
2115
|
+
// Combined push-constant block: vertex bytes 0-63, fragment bytes 64-127.
|
|
2116
|
+
// Issued once per draw instead of two separate vkCmdPushConstants calls
|
|
2117
|
+
// (P2-arg-buffers).
|
|
2118
|
+
struct CombinedPC {
|
|
2119
|
+
TerrainVertexPC v;
|
|
2120
|
+
TerrainFragmentPC f;
|
|
2121
|
+
};
|
|
2122
|
+
static_assert(sizeof(CombinedPC) == 128, "PC layout drift");
|
|
2123
|
+
CombinedPC pc{};
|
|
2124
|
+
|
|
1679
2125
|
for (const auto& draw : frame.draws) {
|
|
1680
2126
|
if (draw.indexCount == 0) continue;
|
|
1681
2127
|
|
|
1682
|
-
// ── Vertex push constants: per-tile RTC MVP (bytes 0-63) ─────────────────
|
|
1683
|
-
TerrainVertexPC vpc{};
|
|
1684
2128
|
const float* mp = glm::value_ptr(draw.mvpMatrix);
|
|
1685
|
-
for (int i = 0; i < 16; ++i)
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
vkCmdPushConstants(cmd, terrainPipelineLayout_, VK_SHADER_STAGE_FRAGMENT_BIT,
|
|
1707
|
-
sizeof(TerrainVertexPC), sizeof(TerrainFragmentPC), &fpc);
|
|
1708
|
-
|
|
1709
|
-
// ── Set 1: imagery overlay ───────────────────────────────────────────────
|
|
2129
|
+
for (int i = 0; i < 16; ++i) pc.v.mvpMatrix[i] = mp[i];
|
|
2130
|
+
|
|
2131
|
+
pc.f.hasOverlay = (draw.overlayTexture && draw.hasUVs) ? 1u : 0u;
|
|
2132
|
+
pc.f.isEllipsoidFallback = draw.isEllipsoidFallback ? 1u : 0u;
|
|
2133
|
+
pc.f.isOnlyWater = draw.isOnlyWater ? 1u : 0u;
|
|
2134
|
+
pc.f.hasWaterMask = (draw.waterMaskTexture != nullptr) ? 1u : 0u;
|
|
2135
|
+
pc.f.wmWest = draw.wmTileBounds.x;
|
|
2136
|
+
pc.f.wmSouth = draw.wmTileBounds.y;
|
|
2137
|
+
pc.f.wmEast = draw.wmTileBounds.z;
|
|
2138
|
+
pc.f.wmNorth = draw.wmTileBounds.w;
|
|
2139
|
+
pc.f.rtcCenterX = draw.rtcCenterEcef.x;
|
|
2140
|
+
pc.f.rtcCenterY = draw.rtcCenterEcef.y;
|
|
2141
|
+
pc.f.rtcCenterZ = draw.rtcCenterEcef.z;
|
|
2142
|
+
pc.f.translationX = draw.overlayTranslation.x;
|
|
2143
|
+
pc.f.translationY = draw.overlayTranslation.y;
|
|
2144
|
+
pc.f.scaleX = draw.overlayScale.x;
|
|
2145
|
+
pc.f.scaleY = draw.overlayScale.y;
|
|
2146
|
+
vkCmdPushConstants(cmd, terrainPipelineLayout_,
|
|
2147
|
+
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
|
|
2148
|
+
0, sizeof(CombinedPC), &pc);
|
|
2149
|
+
|
|
1710
2150
|
VkDescriptorSet texSet = fallbackTexDescSet_;
|
|
1711
2151
|
if (draw.overlayTexture && draw.hasUVs) {
|
|
1712
2152
|
auto* tex = static_cast<VulkanTexture*>(draw.overlayTexture);
|
|
1713
|
-
|
|
2153
|
+
// Skip textures still pending their first GPU upload — would read
|
|
2154
|
+
// undefined data otherwise. Falls back to white for one frame.
|
|
2155
|
+
if (tex && tex->descriptorSet && !tex->pendingUpload) {
|
|
1714
2156
|
texSet = tex->descriptorSet;
|
|
1715
2157
|
}
|
|
1716
2158
|
}
|
|
@@ -1720,11 +2162,10 @@ void VulkanBackend::drawScene(const FrameResult& frame) {
|
|
|
1720
2162
|
1, 1, &lastTexSet, 0, nullptr);
|
|
1721
2163
|
}
|
|
1722
2164
|
|
|
1723
|
-
// ── Set 2: water mask ────────────────────────────────────────────────────
|
|
1724
2165
|
VkDescriptorSet wmSet = fallbackWaterMaskDescSet_;
|
|
1725
2166
|
if (draw.waterMaskTexture) {
|
|
1726
2167
|
auto* wmTex = static_cast<VulkanTexture*>(draw.waterMaskTexture);
|
|
1727
|
-
if (wmTex && wmTex->descriptorSet) {
|
|
2168
|
+
if (wmTex && wmTex->descriptorSet && !wmTex->pendingUpload) {
|
|
1728
2169
|
wmSet = wmTex->descriptorSet;
|
|
1729
2170
|
}
|
|
1730
2171
|
}
|
|
@@ -1748,7 +2189,7 @@ void VulkanBackend::endFrame() {
|
|
|
1748
2189
|
VK_CHECK(vkEndCommandBuffer(cmd));
|
|
1749
2190
|
|
|
1750
2191
|
VkSemaphore waitSemaphores[] = {imageAvailableSemaphores_[frameIndex_]};
|
|
1751
|
-
VkSemaphore signalSemaphores[] = {renderFinishedSemaphores_[
|
|
2192
|
+
VkSemaphore signalSemaphores[] = {renderFinishedSemaphores_[imageIndex_]};
|
|
1752
2193
|
VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
|
|
1753
2194
|
|
|
1754
2195
|
VkSubmitInfo submitInfo{};
|