1 | /* |
2 | Simple DirectMedia Layer |
3 | Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org> |
4 | |
5 | This software is provided 'as-is', without any express or implied |
6 | warranty. In no event will the authors be held liable for any damages |
7 | arising from the use of this software. |
8 | |
9 | Permission is granted to anyone to use this software for any purpose, |
10 | including commercial applications, and to alter it and redistribute it |
11 | freely, subject to the following restrictions: |
12 | |
13 | 1. The origin of this software must not be misrepresented; you must not |
14 | claim that you wrote the original software. If you use this software |
15 | in a product, an acknowledgment in the product documentation would be |
16 | appreciated but is not required. |
17 | 2. Altered source versions must be plainly marked as such, and must not be |
18 | misrepresented as being the original software. |
19 | 3. This notice may not be removed or altered from any source distribution. |
20 | */ |
21 | |
22 | /* |
23 | * @author Manuel Alfayate Corchere <redwindwanderer@gmail.com>. |
24 | * Based on Jacob Lifshay's SDL_x11vulkan.c. |
25 | */ |
26 | |
27 | #include "SDL_internal.h" |
28 | |
29 | #if defined(SDL_VIDEO_VULKAN) && defined(SDL_VIDEO_DRIVER_KMSDRM) |
30 | |
31 | #include "../SDL_vulkan_internal.h" |
32 | |
33 | #include "SDL_kmsdrmvideo.h" |
34 | #include "SDL_kmsdrmdyn.h" |
35 | #include "SDL_kmsdrmvulkan.h" |
36 | |
37 | #include <sys/ioctl.h> |
38 | |
39 | #ifdef SDL_PLATFORM_OPENBSD |
40 | #define DEFAULT_VULKAN "libvulkan.so" |
41 | #else |
42 | #define DEFAULT_VULKAN "libvulkan.so.1" |
43 | #endif |
44 | |
45 | bool KMSDRM_Vulkan_LoadLibrary(SDL_VideoDevice *_this, const char *path) |
46 | { |
47 | VkExtensionProperties *extensions = NULL; |
48 | Uint32 i, extensionCount = 0; |
49 | bool hasSurfaceExtension = false; |
50 | bool hasDisplayExtension = false; |
51 | PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = NULL; |
52 | |
53 | if (_this->vulkan_config.loader_handle) { |
54 | return SDL_SetError("Vulkan already loaded" ); |
55 | } |
56 | |
57 | // Load the Vulkan library |
58 | if (!path) { |
59 | path = SDL_GetHint(SDL_HINT_VULKAN_LIBRARY); |
60 | } |
61 | if (!path) { |
62 | path = DEFAULT_VULKAN; |
63 | } |
64 | |
65 | _this->vulkan_config.loader_handle = SDL_LoadObject(path); |
66 | |
67 | if (!_this->vulkan_config.loader_handle) { |
68 | return false; |
69 | } |
70 | |
71 | SDL_strlcpy(_this->vulkan_config.loader_path, path, |
72 | SDL_arraysize(_this->vulkan_config.loader_path)); |
73 | |
74 | vkGetInstanceProcAddr = (PFN_vkGetInstanceProcAddr)SDL_LoadFunction( |
75 | _this->vulkan_config.loader_handle, "vkGetInstanceProcAddr" ); |
76 | |
77 | if (!vkGetInstanceProcAddr) { |
78 | goto fail; |
79 | } |
80 | |
81 | _this->vulkan_config.vkGetInstanceProcAddr = (void *)vkGetInstanceProcAddr; |
82 | _this->vulkan_config.vkEnumerateInstanceExtensionProperties = |
83 | (void *)((PFN_vkGetInstanceProcAddr)_this->vulkan_config.vkGetInstanceProcAddr)( |
84 | VK_NULL_HANDLE, "vkEnumerateInstanceExtensionProperties" ); |
85 | |
86 | if (!_this->vulkan_config.vkEnumerateInstanceExtensionProperties) { |
87 | goto fail; |
88 | } |
89 | |
90 | extensions = SDL_Vulkan_CreateInstanceExtensionsList( |
91 | (PFN_vkEnumerateInstanceExtensionProperties) |
92 | _this->vulkan_config.vkEnumerateInstanceExtensionProperties, |
93 | &extensionCount); |
94 | |
95 | if (!extensions) { |
96 | goto fail; |
97 | } |
98 | |
99 | for (i = 0; i < extensionCount; i++) { |
100 | if (SDL_strcmp(VK_KHR_SURFACE_EXTENSION_NAME, extensions[i].extensionName) == 0) { |
101 | hasSurfaceExtension = true; |
102 | } else if (SDL_strcmp(VK_KHR_DISPLAY_EXTENSION_NAME, extensions[i].extensionName) == 0) { |
103 | hasDisplayExtension = true; |
104 | } |
105 | } |
106 | |
107 | SDL_free(extensions); |
108 | |
109 | if (!hasSurfaceExtension) { |
110 | SDL_SetError("Installed Vulkan doesn't implement the " VK_KHR_SURFACE_EXTENSION_NAME " extension" ); |
111 | goto fail; |
112 | } else if (!hasDisplayExtension) { |
113 | SDL_SetError("Installed Vulkan doesn't implement the " VK_KHR_DISPLAY_EXTENSION_NAME "extension" ); |
114 | goto fail; |
115 | } |
116 | |
117 | return true; |
118 | |
119 | fail: |
120 | SDL_UnloadObject(_this->vulkan_config.loader_handle); |
121 | _this->vulkan_config.loader_handle = NULL; |
122 | return false; |
123 | } |
124 | |
125 | void KMSDRM_Vulkan_UnloadLibrary(SDL_VideoDevice *_this) |
126 | { |
127 | if (_this->vulkan_config.loader_handle) { |
128 | SDL_UnloadObject(_this->vulkan_config.loader_handle); |
129 | _this->vulkan_config.loader_handle = NULL; |
130 | } |
131 | } |
132 | |
133 | /*********************************************************************/ |
134 | // Here we can put whatever Vulkan extensions we want to be enabled |
135 | // at instance creation, which is done in the programs, not in SDL. |
136 | // So: programs call SDL_Vulkan_GetInstanceExtensions() and here |
137 | // we put the extensions specific to this backend so the programs |
138 | // get a list with the extension we want, so they can include that |
139 | // list in the ppEnabledExtensionNames and EnabledExtensionCount |
140 | // members of the VkInstanceCreateInfo struct passed to |
141 | // vkCreateInstance(). |
142 | /*********************************************************************/ |
143 | char const* const* KMSDRM_Vulkan_GetInstanceExtensions(SDL_VideoDevice *_this, |
144 | Uint32 *count) |
145 | { |
146 | static const char *const extensionsForKMSDRM[] = { |
147 | VK_KHR_SURFACE_EXTENSION_NAME, VK_KHR_DISPLAY_EXTENSION_NAME |
148 | }; |
149 | if (count) { |
150 | *count = SDL_arraysize(extensionsForKMSDRM); |
151 | } |
152 | return extensionsForKMSDRM; |
153 | } |
154 | |
155 | /***********************************************************************/ |
156 | // First thing to know is that we don't call vkCreateInstance() here. |
157 | // Instead, programs using SDL and Vulkan create their Vulkan instance |
158 | // and we get it here, ready to use. |
159 | // Extensions specific for this platform are activated in |
160 | // KMSDRM_Vulkan_GetInstanceExtensions(), like we do with |
161 | // VK_KHR_DISPLAY_EXTENSION_NAME, which is what we need for x-less VK. |
162 | /***********************************************************************/ |
163 | bool KMSDRM_Vulkan_CreateSurface(SDL_VideoDevice *_this, |
164 | SDL_Window *window, |
165 | VkInstance instance, |
166 | const struct VkAllocationCallbacks *allocator, |
167 | VkSurfaceKHR *surface) |
168 | { |
169 | VkPhysicalDevice gpu = NULL; |
170 | uint32_t gpu_count; |
171 | uint32_t display_count; |
172 | uint32_t mode_count; |
173 | uint32_t plane_count; |
174 | uint32_t plane = UINT32_MAX; |
175 | |
176 | VkPhysicalDevice *physical_devices = NULL; |
177 | VkPhysicalDeviceProperties *device_props = NULL; |
178 | VkDisplayPropertiesKHR *display_props = NULL; |
179 | VkDisplayModePropertiesKHR *mode_props = NULL; |
180 | VkDisplayPlanePropertiesKHR *plane_props = NULL; |
181 | VkDisplayPlaneCapabilitiesKHR plane_caps; |
182 | |
183 | VkDisplayModeCreateInfoKHR display_mode_create_info; |
184 | VkDisplaySurfaceCreateInfoKHR display_plane_surface_create_info; |
185 | |
186 | VkExtent2D image_size; |
187 | VkDisplayKHR display; |
188 | VkDisplayModeKHR display_mode = (VkDisplayModeKHR)0; |
189 | VkDisplayModePropertiesKHR display_mode_props = { 0 }; |
190 | VkDisplayModeParametersKHR new_mode_parameters = { { 0, 0 }, 0 }; |
191 | // Prefer a plane that supports per-pixel alpha. |
192 | VkDisplayPlaneAlphaFlagBitsKHR alpha_mode = VK_DISPLAY_PLANE_ALPHA_OPAQUE_BIT_KHR; |
193 | |
194 | VkResult result; |
195 | bool ret = false; |
196 | bool valid_gpu = false; |
197 | bool mode_found = false; |
198 | bool plane_supports_display = false; |
199 | |
200 | // Get the display index from the display being used by the window. |
201 | int display_index = SDL_GetDisplayIndex(SDL_GetDisplayForWindow(window)); |
202 | int i, j; |
203 | |
204 | // Get the function pointers for the functions we will use. |
205 | PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = |
206 | (PFN_vkGetInstanceProcAddr)_this->vulkan_config.vkGetInstanceProcAddr; |
207 | |
208 | PFN_vkCreateDisplayPlaneSurfaceKHR vkCreateDisplayPlaneSurfaceKHR = |
209 | (PFN_vkCreateDisplayPlaneSurfaceKHR)vkGetInstanceProcAddr( |
210 | instance, "vkCreateDisplayPlaneSurfaceKHR" ); |
211 | |
212 | PFN_vkEnumeratePhysicalDevices vkEnumeratePhysicalDevices = |
213 | (PFN_vkEnumeratePhysicalDevices)vkGetInstanceProcAddr( |
214 | instance, "vkEnumeratePhysicalDevices" ); |
215 | |
216 | PFN_vkGetPhysicalDeviceProperties vkGetPhysicalDeviceProperties = |
217 | (PFN_vkGetPhysicalDeviceProperties)vkGetInstanceProcAddr( |
218 | instance, "vkGetPhysicalDeviceProperties" ); |
219 | |
220 | PFN_vkGetPhysicalDeviceDisplayPropertiesKHR vkGetPhysicalDeviceDisplayPropertiesKHR = |
221 | (PFN_vkGetPhysicalDeviceDisplayPropertiesKHR)vkGetInstanceProcAddr( |
222 | instance, "vkGetPhysicalDeviceDisplayPropertiesKHR" ); |
223 | |
224 | PFN_vkGetDisplayModePropertiesKHR vkGetDisplayModePropertiesKHR = |
225 | (PFN_vkGetDisplayModePropertiesKHR)vkGetInstanceProcAddr( |
226 | instance, "vkGetDisplayModePropertiesKHR" ); |
227 | |
228 | PFN_vkGetPhysicalDeviceDisplayPlanePropertiesKHR vkGetPhysicalDeviceDisplayPlanePropertiesKHR = |
229 | (PFN_vkGetPhysicalDeviceDisplayPlanePropertiesKHR)vkGetInstanceProcAddr( |
230 | instance, "vkGetPhysicalDeviceDisplayPlanePropertiesKHR" ); |
231 | |
232 | PFN_vkGetDisplayPlaneSupportedDisplaysKHR vkGetDisplayPlaneSupportedDisplaysKHR = |
233 | (PFN_vkGetDisplayPlaneSupportedDisplaysKHR)vkGetInstanceProcAddr( |
234 | instance, "vkGetDisplayPlaneSupportedDisplaysKHR" ); |
235 | |
236 | PFN_vkGetDisplayPlaneCapabilitiesKHR vkGetDisplayPlaneCapabilitiesKHR = |
237 | (PFN_vkGetDisplayPlaneCapabilitiesKHR)vkGetInstanceProcAddr( |
238 | instance, "vkGetDisplayPlaneCapabilitiesKHR" ); |
239 | |
240 | PFN_vkCreateDisplayModeKHR vkCreateDisplayModeKHR = |
241 | (PFN_vkCreateDisplayModeKHR)vkGetInstanceProcAddr( |
242 | instance, "vkCreateDisplayModeKHR" ); |
243 | |
244 | if (!_this->vulkan_config.loader_handle) { |
245 | SDL_SetError("Vulkan is not loaded" ); |
246 | goto clean; |
247 | } |
248 | |
249 | /*************************************/ |
250 | // Block for vulkan surface creation |
251 | /*************************************/ |
252 | |
253 | /****************************************************************/ |
254 | // If we got vkCreateDisplayPlaneSurfaceKHR() pointer, it means |
255 | // that the VK_KHR_Display extension is active on the instance. |
256 | // That's the central extension we need for x-less VK! |
257 | /****************************************************************/ |
258 | if (!vkCreateDisplayPlaneSurfaceKHR) { |
259 | SDL_SetError(VK_KHR_DISPLAY_EXTENSION_NAME |
260 | " extension is not enabled in the Vulkan instance." ); |
261 | goto clean; |
262 | } |
263 | |
264 | /* A GPU (or physical_device, in vkcube terms) is a physical GPU. |
265 | A machine with more than one video output doesn't need to have more than one GPU, |
266 | like the Pi4 which has 1 GPU and 2 video outputs. |
267 | Just in case, we test that the GPU we choose is Vulkan-capable. |
268 | If there are new reports about VK init failures, hardcode |
269 | gpu = physical_devices[0], instead of probing, and go with that. |
270 | */ |
271 | |
272 | // Get the physical device count. |
273 | vkEnumeratePhysicalDevices(instance, &gpu_count, NULL); |
274 | |
275 | if (gpu_count == 0) { |
276 | SDL_SetError("Vulkan can't find physical devices (gpus)." ); |
277 | goto clean; |
278 | } |
279 | |
280 | // Get the physical devices. |
281 | physical_devices = SDL_malloc(sizeof(VkPhysicalDevice) * gpu_count); |
282 | device_props = SDL_malloc(sizeof(VkPhysicalDeviceProperties)); |
283 | vkEnumeratePhysicalDevices(instance, &gpu_count, physical_devices); |
284 | |
285 | // Iterate on the physical devices. |
286 | for (i = 0; i < gpu_count; i++) { |
287 | |
288 | // Get the physical device properties. |
289 | vkGetPhysicalDeviceProperties( |
290 | physical_devices[i], |
291 | device_props); |
292 | |
293 | // Is this device a real GPU that supports API version 1 at least? |
294 | if (device_props->apiVersion >= 1 && |
295 | (device_props->deviceType == 1 || device_props->deviceType == 2)) { |
296 | gpu = physical_devices[i]; |
297 | valid_gpu = true; |
298 | break; |
299 | } |
300 | } |
301 | |
302 | if (!valid_gpu) { |
303 | SDL_SetError("Vulkan can't find a valid physical device (gpu)." ); |
304 | goto clean; |
305 | } |
306 | |
307 | /* A display is a video output. 1 GPU can have N displays. |
308 | Vulkan only counts the connected displays. |
309 | Get the display count of the GPU. */ |
310 | vkGetPhysicalDeviceDisplayPropertiesKHR(gpu, &display_count, NULL); |
311 | if (display_count == 0) { |
312 | SDL_SetError("Vulkan can't find any displays." ); |
313 | goto clean; |
314 | } |
315 | |
316 | // Get the props of the displays of the physical device. |
317 | display_props = (VkDisplayPropertiesKHR *)SDL_malloc(display_count * sizeof(*display_props)); |
318 | vkGetPhysicalDeviceDisplayPropertiesKHR(gpu, |
319 | &display_count, |
320 | display_props); |
321 | |
322 | // Get the chosen display based on the display index. |
323 | display = display_props[display_index].display; |
324 | |
325 | // Get the list of the display videomodes. |
326 | vkGetDisplayModePropertiesKHR(gpu, |
327 | display, |
328 | &mode_count, NULL); |
329 | |
330 | if (mode_count == 0) { |
331 | SDL_SetError("Vulkan can't find any video modes for display %i (%s)" , 0, |
332 | display_props[display_index].displayName); |
333 | goto clean; |
334 | } |
335 | |
336 | mode_props = (VkDisplayModePropertiesKHR *)SDL_malloc(mode_count * sizeof(*mode_props)); |
337 | vkGetDisplayModePropertiesKHR(gpu, |
338 | display, |
339 | &mode_count, mode_props); |
340 | |
341 | /* Get a video mode equal to the window size among the predefined ones, |
342 | if possible. |
343 | REMEMBER: We have to get a small enough videomode for the window size, |
344 | because videomode determines how big the scanout region is and we can't |
345 | scanout a region bigger than the window (we would be reading past the |
346 | buffer, and Vulkan would give us a confusing VK_ERROR_SURFACE_LOST_KHR). */ |
347 | for (i = 0; i < mode_count; i++) { |
348 | if (mode_props[i].parameters.visibleRegion.width == window->w && |
349 | mode_props[i].parameters.visibleRegion.height == window->h) { |
350 | display_mode_props = mode_props[i]; |
351 | mode_found = true; |
352 | break; |
353 | } |
354 | } |
355 | |
356 | if (mode_found && |
357 | display_mode_props.parameters.visibleRegion.width > 0 && |
358 | display_mode_props.parameters.visibleRegion.height > 0) { |
359 | // Found a suitable mode among the predefined ones. Use that. |
360 | display_mode = display_mode_props.displayMode; |
361 | } else { |
362 | |
363 | /* Couldn't find a suitable mode among the predefined ones, so try to create our own. |
364 | This won't work for some video chips atm (like Pi's VideoCore) so these are limited |
365 | to supported resolutions. Don't try to use "closest" resolutions either, because |
366 | those are often bigger than the window size, thus causing out-of-bunds scanout. */ |
367 | new_mode_parameters.visibleRegion.width = window->w; |
368 | new_mode_parameters.visibleRegion.height = window->h; |
369 | /* SDL (and DRM, if we look at drmModeModeInfo vrefresh) uses plain integer Hz for |
370 | display mode refresh rate, but Vulkan expects higher precision. */ |
371 | new_mode_parameters.refreshRate = (uint32_t)(window->current_fullscreen_mode.refresh_rate * 1000); |
372 | |
373 | SDL_zero(display_mode_create_info); |
374 | display_mode_create_info.sType = VK_STRUCTURE_TYPE_DISPLAY_MODE_CREATE_INFO_KHR; |
375 | display_mode_create_info.parameters = new_mode_parameters; |
376 | result = vkCreateDisplayModeKHR(gpu, |
377 | display, |
378 | &display_mode_create_info, |
379 | NULL, &display_mode); |
380 | if (result != VK_SUCCESS) { |
381 | SDL_SetError("Vulkan couldn't find a predefined mode for that window size and couldn't create a suitable mode." ); |
382 | goto clean; |
383 | } |
384 | } |
385 | |
386 | // Just in case we get here without a display_mode. |
387 | if (!display_mode) { |
388 | SDL_SetError("Vulkan couldn't get a display mode." ); |
389 | goto clean; |
390 | } |
391 | |
392 | // Get the list of the physical device planes. |
393 | vkGetPhysicalDeviceDisplayPlanePropertiesKHR(gpu, &plane_count, NULL); |
394 | if (plane_count == 0) { |
395 | SDL_SetError("Vulkan can't find any planes." ); |
396 | goto clean; |
397 | } |
398 | plane_props = SDL_malloc(sizeof(VkDisplayPlanePropertiesKHR) * plane_count); |
399 | vkGetPhysicalDeviceDisplayPlanePropertiesKHR(gpu, &plane_count, plane_props); |
400 | |
401 | /* Iterate on the list of planes of the physical device |
402 | to find a plane that matches these criteria: |
403 | -It must be compatible with the chosen display + mode. |
404 | -It isn't currently bound to another display. |
405 | -It supports per-pixel alpha, if possible. */ |
406 | for (i = 0; i < plane_count; i++) { |
407 | |
408 | uint32_t supported_displays_count = 0; |
409 | VkDisplayKHR *supported_displays; |
410 | |
411 | // See if the plane is compatible with the current display. |
412 | vkGetDisplayPlaneSupportedDisplaysKHR(gpu, i, &supported_displays_count, NULL); |
413 | if (supported_displays_count == 0) { |
414 | // This plane doesn't support any displays. Continue to the next plane. |
415 | continue; |
416 | } |
417 | |
418 | // Get the list of displays supported by this plane. |
419 | supported_displays = (VkDisplayKHR *)SDL_malloc(sizeof(VkDisplayKHR) * supported_displays_count); |
420 | vkGetDisplayPlaneSupportedDisplaysKHR(gpu, i, |
421 | &supported_displays_count, supported_displays); |
422 | |
423 | /* The plane must be bound to the chosen display, or not in use. |
424 | If none of these is true, iterate to another plane. */ |
425 | if (!((plane_props[i].currentDisplay == display) || (plane_props[i].currentDisplay == VK_NULL_HANDLE))) { |
426 | continue; |
427 | } |
428 | |
429 | /* Iterate the list of displays supported by this plane |
430 | in order to find out if the chosen display is among them. */ |
431 | plane_supports_display = false; |
432 | for (j = 0; j < supported_displays_count; j++) { |
433 | if (supported_displays[j] == display) { |
434 | plane_supports_display = true; |
435 | break; |
436 | } |
437 | } |
438 | |
439 | // Free the list of displays supported by this plane. |
440 | if (supported_displays) { |
441 | SDL_free(supported_displays); |
442 | } |
443 | |
444 | // If the display is not supported by this plane, iterate to the next plane. |
445 | if (!plane_supports_display) { |
446 | continue; |
447 | } |
448 | |
449 | // Want a plane that supports the alpha mode we have chosen. |
450 | vkGetDisplayPlaneCapabilitiesKHR(gpu, display_mode, i, &plane_caps); |
451 | if (plane_caps.supportedAlpha == alpha_mode) { |
452 | // Yep, this plane is alright. |
453 | plane = i; |
454 | break; |
455 | } |
456 | } |
457 | |
458 | // If we couldn't find an appropriate plane, error out. |
459 | if (plane == UINT32_MAX) { |
460 | SDL_SetError("Vulkan couldn't find an appropriate plane." ); |
461 | goto clean; |
462 | } |
463 | |
464 | /********************************************/ |
465 | // Let's finally create the Vulkan surface! |
466 | /********************************************/ |
467 | |
468 | image_size.width = window->w; |
469 | image_size.height = window->h; |
470 | |
471 | SDL_zero(display_plane_surface_create_info); |
472 | display_plane_surface_create_info.sType = VK_STRUCTURE_TYPE_DISPLAY_SURFACE_CREATE_INFO_KHR; |
473 | display_plane_surface_create_info.displayMode = display_mode; |
474 | display_plane_surface_create_info.planeIndex = plane; |
475 | display_plane_surface_create_info.imageExtent = image_size; |
476 | display_plane_surface_create_info.transform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; |
477 | display_plane_surface_create_info.alphaMode = alpha_mode; |
478 | result = vkCreateDisplayPlaneSurfaceKHR(instance, |
479 | &display_plane_surface_create_info, |
480 | allocator, |
481 | surface); |
482 | if (result != VK_SUCCESS) { |
483 | SDL_SetError("vkCreateDisplayPlaneSurfaceKHR failed: %s" , |
484 | SDL_Vulkan_GetResultString(result)); |
485 | goto clean; |
486 | } |
487 | |
488 | ret = true; // success! |
489 | |
490 | clean: |
491 | if (physical_devices) { |
492 | SDL_free(physical_devices); |
493 | } |
494 | if (display_props) { |
495 | SDL_free(display_props); |
496 | } |
497 | if (device_props) { |
498 | SDL_free(device_props); |
499 | } |
500 | if (plane_props) { |
501 | SDL_free(plane_props); |
502 | } |
503 | if (mode_props) { |
504 | SDL_free(mode_props); |
505 | } |
506 | |
507 | return ret; |
508 | } |
509 | |
510 | void KMSDRM_Vulkan_DestroySurface(SDL_VideoDevice *_this, |
511 | VkInstance instance, |
512 | VkSurfaceKHR surface, |
513 | const struct VkAllocationCallbacks *allocator) |
514 | { |
515 | if (_this->vulkan_config.loader_handle) { |
516 | SDL_Vulkan_DestroySurface_Internal(_this->vulkan_config.vkGetInstanceProcAddr, instance, surface, allocator); |
517 | } |
518 | } |
519 | |
520 | #endif |
521 | |