Imported Upstream version 2.0.14
[platform/upstream/SDL.git] / src / video / SDL_vulkan_utils.c
1 /*
2   Simple DirectMedia Layer
3   Copyright (C) 1997-2020 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 #include "SDL_vulkan_internal.h"
24 #include "SDL_error.h"
25
26 /* !!! FIXME: this file doesn't match coding standards for SDL (brace position, etc). */
27
28 #if SDL_VIDEO_VULKAN
29
30 const char *SDL_Vulkan_GetResultString(VkResult result)
31 {
32     switch((int)result)
33     {
34     case VK_SUCCESS:
35         return "VK_SUCCESS";
36     case VK_NOT_READY:
37         return "VK_NOT_READY";
38     case VK_TIMEOUT:
39         return "VK_TIMEOUT";
40     case VK_EVENT_SET:
41         return "VK_EVENT_SET";
42     case VK_EVENT_RESET:
43         return "VK_EVENT_RESET";
44     case VK_INCOMPLETE:
45         return "VK_INCOMPLETE";
46     case VK_ERROR_OUT_OF_HOST_MEMORY:
47         return "VK_ERROR_OUT_OF_HOST_MEMORY";
48     case VK_ERROR_OUT_OF_DEVICE_MEMORY:
49         return "VK_ERROR_OUT_OF_DEVICE_MEMORY";
50     case VK_ERROR_INITIALIZATION_FAILED:
51         return "VK_ERROR_INITIALIZATION_FAILED";
52     case VK_ERROR_DEVICE_LOST:
53         return "VK_ERROR_DEVICE_LOST";
54     case VK_ERROR_MEMORY_MAP_FAILED:
55         return "VK_ERROR_MEMORY_MAP_FAILED";
56     case VK_ERROR_LAYER_NOT_PRESENT:
57         return "VK_ERROR_LAYER_NOT_PRESENT";
58     case VK_ERROR_EXTENSION_NOT_PRESENT:
59         return "VK_ERROR_EXTENSION_NOT_PRESENT";
60     case VK_ERROR_FEATURE_NOT_PRESENT:
61         return "VK_ERROR_FEATURE_NOT_PRESENT";
62     case VK_ERROR_INCOMPATIBLE_DRIVER:
63         return "VK_ERROR_INCOMPATIBLE_DRIVER";
64     case VK_ERROR_TOO_MANY_OBJECTS:
65         return "VK_ERROR_TOO_MANY_OBJECTS";
66     case VK_ERROR_FORMAT_NOT_SUPPORTED:
67         return "VK_ERROR_FORMAT_NOT_SUPPORTED";
68     case VK_ERROR_FRAGMENTED_POOL:
69         return "VK_ERROR_FRAGMENTED_POOL";
70     case VK_ERROR_UNKNOWN:
71         return "VK_ERROR_UNKNOWN";
72     case VK_ERROR_OUT_OF_POOL_MEMORY:
73         return "VK_ERROR_OUT_OF_POOL_MEMORY";
74     case VK_ERROR_INVALID_EXTERNAL_HANDLE:
75         return "VK_ERROR_INVALID_EXTERNAL_HANDLE";
76     case VK_ERROR_FRAGMENTATION:
77         return "VK_ERROR_FRAGMENTATION";
78     case VK_ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS:
79         return "VK_ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS";
80     case VK_ERROR_SURFACE_LOST_KHR:
81         return "VK_ERROR_SURFACE_LOST_KHR";
82     case VK_ERROR_NATIVE_WINDOW_IN_USE_KHR:
83         return "VK_ERROR_NATIVE_WINDOW_IN_USE_KHR";
84     case VK_SUBOPTIMAL_KHR:
85         return "VK_SUBOPTIMAL_KHR";
86     case VK_ERROR_OUT_OF_DATE_KHR:
87         return "VK_ERROR_OUT_OF_DATE_KHR";
88     case VK_ERROR_INCOMPATIBLE_DISPLAY_KHR:
89         return "VK_ERROR_INCOMPATIBLE_DISPLAY_KHR";
90     case VK_ERROR_VALIDATION_FAILED_EXT:
91         return "VK_ERROR_VALIDATION_FAILED_EXT";
92     case VK_ERROR_INVALID_SHADER_NV:
93         return "VK_ERROR_INVALID_SHADER_NV";
94     case VK_ERROR_INCOMPATIBLE_VERSION_KHR:
95         return "VK_ERROR_INCOMPATIBLE_VERSION_KHR";
96     case VK_ERROR_INVALID_DRM_FORMAT_MODIFIER_PLANE_LAYOUT_EXT:
97         return "VK_ERROR_INVALID_DRM_FORMAT_MODIFIER_PLANE_LAYOUT_EXT";
98     case VK_ERROR_NOT_PERMITTED_EXT:
99         return "VK_ERROR_NOT_PERMITTED_EXT";
100     case VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT:
101         return "VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT";
102     case VK_THREAD_IDLE_KHR:
103         return "VK_THREAD_IDLE_KHR";
104     case VK_THREAD_DONE_KHR:
105         return "VK_THREAD_DONE_KHR";
106     case VK_OPERATION_DEFERRED_KHR:
107         return "VK_OPERATION_DEFERRED_KHR";
108     case VK_OPERATION_NOT_DEFERRED_KHR:
109         return "VK_OPERATION_NOT_DEFERRED_KHR";
110     case VK_PIPELINE_COMPILE_REQUIRED_EXT:
111         return "VK_PIPELINE_COMPILE_REQUIRED_EXT";
112     default:
113         break;
114     }
115     if(result < 0)
116         return "VK_ERROR_<Unknown>";
117     return "VK_<Unknown>";
118 }
119
120 VkExtensionProperties *SDL_Vulkan_CreateInstanceExtensionsList(
121     PFN_vkEnumerateInstanceExtensionProperties vkEnumerateInstanceExtensionProperties,
122     Uint32 *extensionCount)
123 {
124     Uint32 count = 0;
125     VkResult result = vkEnumerateInstanceExtensionProperties(NULL, &count, NULL);
126     VkExtensionProperties *retval;
127     if(result == VK_ERROR_INCOMPATIBLE_DRIVER)
128     {
129         /* Avoid the ERR_MAX_STRLEN limit by passing part of the message
130          * as a string argument.
131          */
132         SDL_SetError(
133             "You probably don't have a working Vulkan driver installed. %s %s %s(%d)",
134             "Getting Vulkan extensions failed:",
135             "vkEnumerateInstanceExtensionProperties returned",
136             SDL_Vulkan_GetResultString(result),
137             (int)result);
138         return NULL;
139     }
140     else if(result != VK_SUCCESS)
141     {
142         SDL_SetError(
143             "Getting Vulkan extensions failed: vkEnumerateInstanceExtensionProperties returned "
144             "%s(%d)",
145             SDL_Vulkan_GetResultString(result),
146             (int)result);
147         return NULL;
148     }
149     if(count == 0)
150     {
151         retval = SDL_calloc(1, sizeof(VkExtensionProperties)); // so we can return non-null
152     }
153     else
154     {
155         retval = SDL_calloc(count, sizeof(VkExtensionProperties));
156     }
157     if(!retval)
158     {
159         SDL_OutOfMemory();
160         return NULL;
161     }
162     result = vkEnumerateInstanceExtensionProperties(NULL, &count, retval);
163     if(result != VK_SUCCESS)
164     {
165         SDL_SetError(
166             "Getting Vulkan extensions failed: vkEnumerateInstanceExtensionProperties returned "
167             "%s(%d)",
168             SDL_Vulkan_GetResultString(result),
169             (int)result);
170         SDL_free(retval);
171         return NULL;
172     }
173     *extensionCount = count;
174     return retval;
175 }
176
177 SDL_bool SDL_Vulkan_GetInstanceExtensions_Helper(unsigned *userCount,
178                                                  const char **userNames,
179                                                  unsigned nameCount,
180                                                  const char *const *names)
181 {
182     if (userNames) {
183         unsigned i;
184
185         if (*userCount < nameCount) {
186             SDL_SetError("Output array for SDL_Vulkan_GetInstanceExtensions needs to be at least %d big", nameCount);
187             return SDL_FALSE;
188         }
189         for (i = 0; i < nameCount; i++) {
190             userNames[i] = names[i];
191         }
192     }
193     *userCount = nameCount;
194     return SDL_TRUE;
195 }
196
197 /* Alpha modes, in order of preference */
198 static const VkDisplayPlaneAlphaFlagBitsKHR alphaModes[4] = {
199     VK_DISPLAY_PLANE_ALPHA_OPAQUE_BIT_KHR,
200     VK_DISPLAY_PLANE_ALPHA_GLOBAL_BIT_KHR,
201     VK_DISPLAY_PLANE_ALPHA_PER_PIXEL_BIT_KHR,
202     VK_DISPLAY_PLANE_ALPHA_PER_PIXEL_PREMULTIPLIED_BIT_KHR,
203 };
204
205 SDL_bool SDL_Vulkan_Display_CreateSurface(void *vkGetInstanceProcAddr_,
206                                   VkInstance instance,
207                                   VkSurfaceKHR *surface)
208 {
209     PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr =
210         (PFN_vkGetInstanceProcAddr)vkGetInstanceProcAddr_;
211 #define VULKAN_INSTANCE_FUNCTION(name)                                           \
212     PFN_##name name = (PFN_##name)vkGetInstanceProcAddr((VkInstance)instance, #name)
213     VULKAN_INSTANCE_FUNCTION(vkEnumeratePhysicalDevices);
214     VULKAN_INSTANCE_FUNCTION(vkGetPhysicalDeviceDisplayPropertiesKHR);
215     VULKAN_INSTANCE_FUNCTION(vkGetDisplayModePropertiesKHR);
216     VULKAN_INSTANCE_FUNCTION(vkGetPhysicalDeviceDisplayPlanePropertiesKHR);
217     VULKAN_INSTANCE_FUNCTION(vkGetDisplayPlaneCapabilitiesKHR);
218     VULKAN_INSTANCE_FUNCTION(vkGetDisplayPlaneSupportedDisplaysKHR);
219     VULKAN_INSTANCE_FUNCTION(vkCreateDisplayPlaneSurfaceKHR);
220 #undef VULKAN_INSTANCE_FUNCTION
221     VkDisplaySurfaceCreateInfoKHR createInfo;
222     VkResult result;
223     uint32_t physicalDeviceCount = 0;
224     VkPhysicalDevice *physicalDevices = NULL;
225     uint32_t physicalDeviceIndex;
226     const char *chosenDisplayId;
227     int displayId = 0; /* Counting from physical device 0, display 0 */
228
229     if(!vkEnumeratePhysicalDevices ||
230        !vkGetPhysicalDeviceDisplayPropertiesKHR ||
231        !vkGetDisplayModePropertiesKHR ||
232        !vkGetPhysicalDeviceDisplayPlanePropertiesKHR ||
233        !vkGetDisplayPlaneCapabilitiesKHR ||
234        !vkGetDisplayPlaneSupportedDisplaysKHR ||
235        !vkCreateDisplayPlaneSurfaceKHR)
236     {
237         SDL_SetError(VK_KHR_DISPLAY_EXTENSION_NAME
238                      " extension is not enabled in the Vulkan instance.");
239         goto error;
240     }
241
242     if ((chosenDisplayId = SDL_getenv("SDL_VULKAN_DISPLAY")) != NULL)
243     {
244         displayId = SDL_atoi(chosenDisplayId);
245     }
246
247     /* Enumerate physical devices */
248     result =
249         vkEnumeratePhysicalDevices(instance, &physicalDeviceCount, NULL);
250     if(result != VK_SUCCESS)
251     {
252         SDL_SetError("Could not enumerate Vulkan physical devices");
253         goto error;
254     }
255     if(physicalDeviceCount == 0)
256     {
257         SDL_SetError("No Vulkan physical devices");
258         goto error;
259     }
260     physicalDevices = SDL_malloc(sizeof(VkPhysicalDevice) * physicalDeviceCount);
261     if(!physicalDevices)
262     {
263         SDL_OutOfMemory();
264         goto error;
265     }
266     result =
267         vkEnumeratePhysicalDevices(instance, &physicalDeviceCount, physicalDevices);
268     if(result != VK_SUCCESS)
269     {
270         SDL_SetError("Error enumerating physical devices");
271         goto error;
272     }
273
274     for(physicalDeviceIndex = 0; physicalDeviceIndex < physicalDeviceCount;
275         physicalDeviceIndex++)
276     {
277                 VkPhysicalDevice physicalDevice = physicalDevices[physicalDeviceIndex];
278         uint32_t displayPropertiesCount = 0;
279         VkDisplayPropertiesKHR *displayProperties = NULL;
280         uint32_t displayModePropertiesCount = 0;
281         VkDisplayModePropertiesKHR *displayModeProperties = NULL;
282         int bestMatchIndex = -1;
283         uint32_t refreshRate = 0;
284         uint32_t i;
285         uint32_t displayPlanePropertiesCount = 0;
286         int planeIndex = -1;
287         VkDisplayKHR display;
288         VkDisplayPlanePropertiesKHR *displayPlaneProperties = NULL;
289         VkExtent2D extent;
290         VkDisplayPlaneCapabilitiesKHR planeCaps;
291
292         /* Get information about the physical displays */
293         result =
294             vkGetPhysicalDeviceDisplayPropertiesKHR(physicalDevice, &displayPropertiesCount, NULL);
295         if (result != VK_SUCCESS || displayPropertiesCount == 0)
296         {
297             /* This device has no physical device display properties, move on to next. */
298             continue;
299         }
300         SDL_LogDebug(SDL_LOG_CATEGORY_VIDEO, "vulkandisplay: Number of display properties for device %u: %u",
301                 physicalDeviceIndex, displayPropertiesCount);
302
303         if ( (displayId < 0) || (((uint32_t) displayId) >= displayPropertiesCount) )
304         {
305             /* Display id specified was higher than number of available displays, move to next physical device. */
306             displayId -= displayPropertiesCount;
307             continue;
308         }
309
310         displayProperties = SDL_malloc(sizeof(VkDisplayPropertiesKHR) * displayPropertiesCount);
311         if(!displayProperties)
312         {
313             SDL_OutOfMemory();
314             goto error;
315         }
316
317         result =
318             vkGetPhysicalDeviceDisplayPropertiesKHR(physicalDevice, &displayPropertiesCount, displayProperties);
319         if (result != VK_SUCCESS || displayPropertiesCount == 0) {
320             SDL_free(displayProperties);
321             SDL_SetError("Error enumerating physical device displays");
322             goto error;
323         }
324
325         display = displayProperties[displayId].display;
326         extent = displayProperties[displayId].physicalResolution;
327         SDL_LogDebug(SDL_LOG_CATEGORY_VIDEO, "vulkandisplay: Display: %s Native resolution: %ux%u",
328                 displayProperties[displayId].displayName, extent.width, extent.height);
329
330         SDL_free(displayProperties);
331         displayProperties = NULL;
332
333         /* Get display mode properties for the chosen display */
334         result =
335             vkGetDisplayModePropertiesKHR(physicalDevice, display, &displayModePropertiesCount, NULL);
336         if (result != VK_SUCCESS || displayModePropertiesCount == 0)
337         {
338             SDL_SetError("Error enumerating display modes");
339             goto error;
340         }
341         SDL_LogDebug(SDL_LOG_CATEGORY_VIDEO, "vulkandisplay: Number of display modes: %u", displayModePropertiesCount);
342
343         displayModeProperties = SDL_malloc(sizeof(VkDisplayModePropertiesKHR) * displayModePropertiesCount);
344         if(!displayModeProperties)
345         {
346             SDL_OutOfMemory();
347             goto error;
348         }
349
350         result =
351             vkGetDisplayModePropertiesKHR(physicalDevice, display, &displayModePropertiesCount, displayModeProperties);
352         if (result != VK_SUCCESS || displayModePropertiesCount == 0) {
353             SDL_SetError("Error enumerating display modes");
354             SDL_free(displayModeProperties);
355             goto error;
356         }
357
358         /* Try to find a display mode that matches the native resolution */
359         for (i = 0; i < displayModePropertiesCount; ++i)
360         {
361             if (displayModeProperties[i].parameters.visibleRegion.width == extent.width &&
362                 displayModeProperties[i].parameters.visibleRegion.height == extent.height &&
363                 displayModeProperties[i].parameters.refreshRate > refreshRate)
364             {
365                 bestMatchIndex = i;
366                 refreshRate = displayModeProperties[i].parameters.refreshRate;
367             }
368         }
369         if (bestMatchIndex < 0)
370         {
371             SDL_SetError("Found no matching display mode");
372             SDL_free(displayModeProperties);
373             goto error;
374         }
375
376         SDL_zero(createInfo);
377         createInfo.displayMode = displayModeProperties[bestMatchIndex].displayMode;
378         SDL_LogDebug(SDL_LOG_CATEGORY_VIDEO, "vulkandisplay: Matching mode %ux%u with refresh rate %u",
379                 displayModeProperties[bestMatchIndex].parameters.visibleRegion.width,
380                 displayModeProperties[bestMatchIndex].parameters.visibleRegion.height,
381                 refreshRate);
382
383         SDL_free(displayModeProperties);
384         displayModeProperties = NULL;
385
386         /* Try to find a plane index that supports our display */
387         result =
388             vkGetPhysicalDeviceDisplayPlanePropertiesKHR(physicalDevice, &displayPlanePropertiesCount, NULL);
389         if (result != VK_SUCCESS || displayPlanePropertiesCount == 0)
390         {
391             SDL_SetError("Error enumerating display planes");
392             goto error;
393         }
394         SDL_LogDebug(SDL_LOG_CATEGORY_VIDEO, "vulkandisplay: Number of display planes: %u", displayPlanePropertiesCount);
395
396         displayPlaneProperties = SDL_malloc(sizeof(VkDisplayPlanePropertiesKHR) * displayPlanePropertiesCount);
397         if(!displayPlaneProperties)
398         {
399             SDL_OutOfMemory();
400             goto error;
401         }
402
403         result =
404             vkGetPhysicalDeviceDisplayPlanePropertiesKHR(physicalDevice, &displayPlanePropertiesCount, displayPlaneProperties);
405         if (result != VK_SUCCESS || displayPlanePropertiesCount == 0)
406         {
407             SDL_SetError("Error enumerating display plane properties");
408             SDL_free(displayPlaneProperties);
409             goto error;
410         }
411
412         for (i = 0; i < displayPlanePropertiesCount; ++i)
413         {
414             uint32_t planeSupportedDisplaysCount = 0;
415             VkDisplayKHR *planeSupportedDisplays = NULL;
416             uint32_t j;
417
418             /* Check if plane is attached to a display, if not, continue. */
419             if (displayPlaneProperties[i].currentDisplay == VK_NULL_HANDLE)
420                 continue;
421
422             /* Check supported displays for this plane. */
423             result =
424                 vkGetDisplayPlaneSupportedDisplaysKHR(physicalDevice, i, &planeSupportedDisplaysCount, NULL);
425             if (result != VK_SUCCESS || planeSupportedDisplaysCount == 0)
426             {
427                 continue;  /* No supported displays, on to next plane. */
428             }
429             SDL_LogDebug(SDL_LOG_CATEGORY_VIDEO, "vulkandisplay: Number of supported displays for plane %u: %u", i, planeSupportedDisplaysCount);
430
431             planeSupportedDisplays = SDL_malloc(sizeof(VkDisplayKHR) * planeSupportedDisplaysCount);
432             if(!planeSupportedDisplays)
433             {
434                 SDL_free(displayPlaneProperties);
435                 SDL_OutOfMemory();
436                 goto error;
437             }
438
439             result =
440                 vkGetDisplayPlaneSupportedDisplaysKHR(physicalDevice, i, &planeSupportedDisplaysCount, planeSupportedDisplays);
441             if (result != VK_SUCCESS || planeSupportedDisplaysCount == 0)
442             {
443                 SDL_SetError("Error enumerating supported displays, or no supported displays");
444                 SDL_free(planeSupportedDisplays);
445                 SDL_free(displayPlaneProperties);
446                 goto error;
447             }
448
449             for (j = 0; j < planeSupportedDisplaysCount && planeSupportedDisplays[j] != display; ++j)
450                 ;
451
452             SDL_free(planeSupportedDisplays);
453             planeSupportedDisplays = NULL;
454
455             if (j == planeSupportedDisplaysCount)
456             {
457                 /* This display is not supported for this plane, move on. */
458                 continue;
459             }
460
461             result = vkGetDisplayPlaneCapabilitiesKHR(physicalDevice, createInfo.displayMode, i, &planeCaps);
462             if (result != VK_SUCCESS)
463             {
464                 SDL_SetError("Error getting display plane capabilities");
465                 SDL_free(displayPlaneProperties);
466                 goto error;
467             }
468
469             /* Check if plane fulfills extent requirements. */
470             if (extent.width >= planeCaps.minDstExtent.width && extent.height >= planeCaps.minDstExtent.height &&
471                 extent.width <= planeCaps.maxDstExtent.width && extent.height <= planeCaps.maxDstExtent.height)
472             {
473                 /* If it does, choose this plane. */
474                 SDL_LogDebug(SDL_LOG_CATEGORY_VIDEO, "vulkandisplay: Choosing plane %d, minimum extent %dx%d maximum extent %dx%d", i,
475                         planeCaps.minDstExtent.width, planeCaps.minDstExtent.height,
476                         planeCaps.maxDstExtent.width, planeCaps.maxDstExtent.height);
477                 planeIndex = i;
478                 break;
479             }
480         }
481
482         if (planeIndex < 0)
483         {
484             SDL_SetError("No plane supports the selected resolution");
485             SDL_free(displayPlaneProperties);
486             goto error;
487         }
488
489         createInfo.planeIndex = planeIndex;
490         createInfo.planeStackIndex = displayPlaneProperties[planeIndex].currentStackIndex;
491         SDL_free(displayPlaneProperties);
492         displayPlaneProperties = NULL;
493
494         /* Find a supported alpha mode. Not all planes support OPAQUE */
495         createInfo.alphaMode = VK_DISPLAY_PLANE_ALPHA_OPAQUE_BIT_KHR;
496         for (i = 0; i < SDL_arraysize(alphaModes); i++) {
497             if (planeCaps.supportedAlpha & alphaModes[i]) {
498                 createInfo.alphaMode = alphaModes[i];
499                 break;
500             }
501         }
502         SDL_LogDebug(SDL_LOG_CATEGORY_VIDEO, "vulkandisplay: Chose alpha mode 0x%x", createInfo.alphaMode);
503
504         /* Found a match, finally! Fill in extent, and break from loop */
505         createInfo.imageExtent = extent;
506         break;
507     }
508
509     SDL_free(physicalDevices);
510     physicalDevices = NULL;
511
512     if (physicalDeviceIndex == physicalDeviceCount)
513     {
514         SDL_SetError("No usable displays found or requested display out of range");
515         return SDL_FALSE;
516     }
517
518     createInfo.sType = VK_STRUCTURE_TYPE_DISPLAY_SURFACE_CREATE_INFO_KHR;
519     createInfo.transform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
520     createInfo.globalAlpha = 1.0f;
521
522     result = vkCreateDisplayPlaneSurfaceKHR(instance, &createInfo,
523                                        NULL, surface);
524     if(result != VK_SUCCESS)
525     {
526         SDL_SetError("vkCreateDisplayPlaneSurfaceKHR failed: %s",
527                      SDL_Vulkan_GetResultString(result));
528         return SDL_FALSE;
529     }
530     SDL_LogDebug(SDL_LOG_CATEGORY_VIDEO, "vulkandisplay: Created surface");
531     return SDL_TRUE;
532 error:
533     SDL_free(physicalDevices);
534     return SDL_FALSE;
535 }
536
537 #endif
538
539 /* vi: set ts=4 sw=4 expandtab: */