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 | |
29 | static 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 | |
59 | bool 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 | |
158 | fail: |
159 | SDL_UnloadObject(_this->vulkan_config.loader_handle); |
160 | _this->vulkan_config.loader_handle = NULL; |
161 | return false; |
162 | } |
163 | |
164 | void 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 | |
172 | char 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 | |
217 | bool 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 | |
251 | void 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 | |