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#include "SDL_internal.h"
22
23#if defined(SDL_VIDEO_VULKAN) && defined(SDL_VIDEO_DRIVER_OFFSCREEN)
24
25#include "../SDL_vulkan_internal.h"
26#include "../SDL_sysvideo.h"
27
28
29static const char *s_defaultPaths[] = {
30#if defined(SDL_PLATFORM_WINDOWS)
31 "vulkan-1.dll"
32#elif defined(SDL_PLATFORM_APPLE)
33 "vulkan.framework/vulkan",
34 "libvulkan.1.dylib",
35 "libvulkan.dylib",
36 "MoltenVK.framework/MoltenVK",
37 "libMoltenVK.dylib"
38#elif defined(SDL_PLATFORM_OPENBSD)
39 "libvulkan.so"
40#else
41 "libvulkan.so.1"
42#endif
43};
44
45#if defined( SDL_PLATFORM_APPLE )
46#include <dlfcn.h>
47
48// Since libSDL is most likely a .dylib, need RTLD_DEFAULT not RTLD_SELF.
49#define DEFAULT_HANDLE RTLD_DEFAULT
50#endif
51
52/*Should the whole driver fail if it can't create a surface? Rendering to an offscreen buffer is still possible without a surface.
53 At the time of writing. I need the driver to minimally work even if the surface extension isn't present.
54 And account for the inability to create a surface on the consumer side.
55 So for now I'm targeting my specific use case -Dave Kircher*/
56#define HEADLESS_SURFACE_EXTENSION_REQUIRED_TO_LOAD 0
57
58
59bool OFFSCREEN_Vulkan_LoadLibrary(SDL_VideoDevice *_this, const char *path)
60{
61 VkExtensionProperties *extensions = NULL;
62 Uint32 extensionCount = 0;
63 bool hasSurfaceExtension = false;
64 bool hasHeadlessSurfaceExtension = false;
65 PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = NULL;
66 Uint32 i;
67 const char **paths;
68 const char *foundPath = NULL;
69 Uint32 numPaths;
70
71 if (_this->vulkan_config.loader_handle) {
72 return SDL_SetError("Vulkan already loaded");
73 }
74
75 // Load the Vulkan loader library
76 if (!path) {
77 path = SDL_GetHint(SDL_HINT_VULKAN_LIBRARY);
78 }
79
80#if defined(SDL_PLATFORM_APPLE)
81 if (!path) {
82 // Handle the case where Vulkan Portability is linked statically.
83 vkGetInstanceProcAddr =
84 (PFN_vkGetInstanceProcAddr)dlsym(DEFAULT_HANDLE,
85 "vkGetInstanceProcAddr");
86 }
87
88 if (vkGetInstanceProcAddr) {
89 _this->vulkan_config.loader_handle = DEFAULT_HANDLE;
90 } else
91#endif
92 {
93 if (path) {
94 paths = &path;
95 numPaths = 1;
96 } else {
97 paths = s_defaultPaths;
98 numPaths = SDL_arraysize(s_defaultPaths);
99 }
100
101 for (i = 0; i < numPaths && _this->vulkan_config.loader_handle == NULL; i++) {
102 foundPath = paths[i];
103 _this->vulkan_config.loader_handle = SDL_LoadObject(foundPath);
104 }
105
106 if (_this->vulkan_config.loader_handle == NULL) {
107 return SDL_SetError("Failed to load Vulkan Portability library");
108 }
109
110 SDL_strlcpy(_this->vulkan_config.loader_path, foundPath,
111 SDL_arraysize(_this->vulkan_config.loader_path));
112 vkGetInstanceProcAddr = (PFN_vkGetInstanceProcAddr)SDL_LoadFunction(
113 _this->vulkan_config.loader_handle, "vkGetInstanceProcAddr");
114
115 if (!vkGetInstanceProcAddr) {
116 SDL_SetError("Failed to load vkGetInstanceProcAddr from Vulkan Portability library");
117 goto fail;
118 }
119 }
120
121 _this->vulkan_config.vkGetInstanceProcAddr = (SDL_FunctionPointer)vkGetInstanceProcAddr;
122 _this->vulkan_config.vkEnumerateInstanceExtensionProperties =
123 (SDL_FunctionPointer)((PFN_vkGetInstanceProcAddr)_this->vulkan_config.vkGetInstanceProcAddr)(
124 VK_NULL_HANDLE, "vkEnumerateInstanceExtensionProperties");
125 if (!_this->vulkan_config.vkEnumerateInstanceExtensionProperties) {
126 goto fail;
127 }
128 extensions = SDL_Vulkan_CreateInstanceExtensionsList(
129 (PFN_vkEnumerateInstanceExtensionProperties)
130 _this->vulkan_config.vkEnumerateInstanceExtensionProperties,
131 &extensionCount);
132 if (!extensions) {
133 goto fail;
134 }
135 for (i = 0; i < extensionCount; i++) {
136 if (SDL_strcmp(VK_KHR_SURFACE_EXTENSION_NAME, extensions[i].extensionName) == 0) {
137 hasSurfaceExtension = true;
138 } else if (SDL_strcmp(VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME, extensions[i].extensionName) == 0) {
139 hasHeadlessSurfaceExtension = true;
140 }
141 }
142 SDL_free(extensions);
143 if (!hasSurfaceExtension) {
144 SDL_SetError("Installed Vulkan doesn't implement the " VK_KHR_SURFACE_EXTENSION_NAME " extension");
145 goto fail;
146 }
147 if (!hasHeadlessSurfaceExtension) {
148#if (HEADLESS_SURFACE_EXTENSION_REQUIRED_TO_LOAD != 0)
149 SDL_SetError("Installed Vulkan doesn't implement the " VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME " extension");
150 goto fail;
151#else
152 // Let's at least leave a breadcrumb for people to find if they have issues
153 SDL_Log("Installed Vulkan doesn't implement the " VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME " extension");
154#endif
155 }
156 return true;
157
158fail:
159 SDL_UnloadObject(_this->vulkan_config.loader_handle);
160 _this->vulkan_config.loader_handle = NULL;
161 return false;
162}
163
164void OFFSCREEN_Vulkan_UnloadLibrary(SDL_VideoDevice *_this)
165{
166 if (_this->vulkan_config.loader_handle) {
167 SDL_UnloadObject(_this->vulkan_config.loader_handle);
168 _this->vulkan_config.loader_handle = NULL;
169 }
170}
171
172char const *const *OFFSCREEN_Vulkan_GetInstanceExtensions(SDL_VideoDevice *_this,
173 Uint32 *count)
174{
175#if (HEADLESS_SURFACE_EXTENSION_REQUIRED_TO_LOAD == 0)
176 VkExtensionProperties *enumerateExtensions = NULL;
177 Uint32 enumerateExtensionCount = 0;
178 bool hasHeadlessSurfaceExtension = false;
179 Uint32 i;
180#endif
181
182 static const char *const returnExtensions[] = { VK_KHR_SURFACE_EXTENSION_NAME, VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME };
183 if (count) {
184# if (HEADLESS_SURFACE_EXTENSION_REQUIRED_TO_LOAD == 0)
185 {
186 /* In optional mode, only return VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME if it's already supported by the instance
187 There's probably a better way to cache the presence of the extension during OFFSCREEN_Vulkan_LoadLibrary().
188 But both SDL_VideoData and SDL_VideoDevice::vulkan_config seem like I'd need to touch a bunch of code to do properly.
189 And I want a smaller footprint for the first pass*/
190 if ( _this->vulkan_config.vkEnumerateInstanceExtensionProperties ) {
191 enumerateExtensions = SDL_Vulkan_CreateInstanceExtensionsList(
192 (PFN_vkEnumerateInstanceExtensionProperties)
193 _this->vulkan_config.vkEnumerateInstanceExtensionProperties,
194 &enumerateExtensionCount);
195 for (i = 0; i < enumerateExtensionCount; i++) {
196 if (SDL_strcmp(VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME, enumerateExtensions[i].extensionName) == 0) {
197 hasHeadlessSurfaceExtension = true;
198 }
199 }
200 SDL_free(enumerateExtensions);
201 }
202 if ( hasHeadlessSurfaceExtension == true ) {
203 *count = SDL_arraysize(returnExtensions);
204 } else {
205 *count = SDL_arraysize(returnExtensions) - 1; // assumes VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME is last
206 }
207 }
208# else
209 {
210 *count = SDL_arraysize(returnExtensions);
211 }
212# endif
213 }
214 return returnExtensions;
215}
216
217bool OFFSCREEN_Vulkan_CreateSurface(SDL_VideoDevice *_this,
218 SDL_Window *window,
219 VkInstance instance,
220 const struct VkAllocationCallbacks *allocator,
221 VkSurfaceKHR *surface)
222{
223 *surface = VK_NULL_HANDLE;
224
225 if (!_this->vulkan_config.loader_handle) {
226 return SDL_SetError("Vulkan is not loaded");
227 }
228
229 PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = (PFN_vkGetInstanceProcAddr)_this->vulkan_config.vkGetInstanceProcAddr;
230 PFN_vkCreateHeadlessSurfaceEXT vkCreateHeadlessSurfaceEXT =
231 (PFN_vkCreateHeadlessSurfaceEXT)vkGetInstanceProcAddr(instance, "vkCreateHeadlessSurfaceEXT");
232 VkHeadlessSurfaceCreateInfoEXT createInfo;
233 VkResult result;
234 if (!vkCreateHeadlessSurfaceEXT) {
235 /* This may be surprising to the consumer when HEADLESS_SURFACE_EXTENSION_REQUIRED_TO_LOAD == 0
236 But this is the tradeoff for allowing offscreen rendering to a buffer to continue working without requiring the extension during driver load */
237 return SDL_SetError(VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME
238 " extension is not enabled in the Vulkan instance.");
239 }
240 SDL_zero(createInfo);
241 createInfo.sType = VK_STRUCTURE_TYPE_HEADLESS_SURFACE_CREATE_INFO_EXT;
242 createInfo.pNext = NULL;
243 createInfo.flags = 0;
244 result = vkCreateHeadlessSurfaceEXT(instance, &createInfo, allocator, surface);
245 if (result != VK_SUCCESS) {
246 return SDL_SetError("vkCreateHeadlessSurfaceEXT failed: %s", SDL_Vulkan_GetResultString(result));
247 }
248 return true;
249}
250
251void OFFSCREEN_Vulkan_DestroySurface(SDL_VideoDevice *_this,
252 VkInstance instance,
253 VkSurfaceKHR surface,
254 const struct VkAllocationCallbacks *allocator)
255{
256 if (_this->vulkan_config.loader_handle) {
257 SDL_Vulkan_DestroySurface_Internal(_this->vulkan_config.vkGetInstanceProcAddr, instance, surface, allocator);
258 }
259}
260
261#endif
262