Vulkan(0)搭建环境-清空窗口
认识Vulkan
Vulkan是新一代3D图形API,它继承了OpenGL的优点,弥补了OpenGL的缺憾。有点像科创板之于主板,歼20之于歼10,微信之于QQ,网店之于实体店,今日之于昨日。
使用OpenGL时,每次drawcall都需要向OpenGL提交很多数据。而Vulkan可以提前将这些drawcall指令保存到一个buffer(像保存顶点数据到buffer一样),这样就减少了很多开销。
使用OpenGL时,OpenGL的Context会包含很多你并不打算使用的东西,例如线的宽度、混合等。而Vulkan不会提供这些你用不到的东西,你需要什么,你来指定。(当然,你不指定,Vulkan不会自动地提供)
Vulkan还支持多线程,OpenGL这方面就不行了。
Vulkan对GPU的抽象比OpenGL更加细腻。
搭建环境
本文和本系列都将使用C#和Visual Studio 2017来学习使用Vulkan。
首先,在官网()下载vulkan-sdk.exe和vulkan-runtime.exe。完后安装。vulkan-runtime.exe也可以在()下载。vulkan-sdk.exe太大,我就不提供下载了。
然后,下载Vulkan.net库()。这是本人搜罗整理来的一个Vulkan库,外加一些示例代码。用VS2017打开,在这个解决方案下就可以学习使用Vulkan了。
如果读者在Github上的下载速度太慢,可以试试将各个文件单独点开下载。这很笨,但也是个办法。
简单介绍下此解决方案。
Vulkan文件夹下的是对Vulkan API的封装。Vulkan使用了大量的struct、enum,这与OpenGL类似。
Vulkan.Platforms文件夹下的是平台相关的一些API。
Lesson01Clear文件夹下的是第一个示例,展示了Vulkan清空窗口的代码。以后会逐步添加更多的示例。
有了这个库,读者就可以运行示例程序,一点点地读代码,慢慢理解Vulkan了。这也是本人用的最多的学习方法。遇到不懂的就上网搜索,毕竟我没有别人可以问。
这个库还很不成熟,以后会有大的改动。但这不妨碍学习,反而是学习的好资料,在变动的过程中方能体会软件工程的精髓。
清空窗口
用Vulkan写个清空窗口的程序,就像是用C写个hello world。
外壳
新建Windows窗体应用程序。
添加对类库Vulkan和Vulkan.Platforms的引用:
添加此项目的核心类型LessonClear。Vulkan需要初始化(Init)一些东西,在每次渲染时,渲染(Render)一些东西。
1 namespace Lesson01Clear { 2 unsafe class LessonClear { 3 4 bool isInitialized = false; 5 6 public void Init() { 7 if (this.isInitialized) { return; } 8 9 this.isInitialized = true;10 }11 12 public void Render() {13 if (!isInitialized) return;14 15 }16 }17 }
添加一个User Control,用以调用LessonClear。
1 namespace Lesson01Clear { 2 public partial class UCClear : UserControl { 3 4 LessonClear lesson; 5 6 public UCClear() { 7 InitializeComponent(); 8 } 9 10 protected override void OnLoad(EventArgs e) {11 base.OnLoad(e);12 13 this.lesson = new LessonClear();14 this.lesson.Init();15 }16 17 protected override void OnPaintBackground(PaintEventArgs e) {18 var lesson = this.lesson;19 if (lesson != null) {20 lesson.Render();21 }22 }23 }24 }
在主窗口中添加一个自定义控件UCClear。这样,在窗口启动时,就会自动执行LessonClear的初始化和渲染功能了。
此时的解决方案如下:
初始化
要初始化的东西比较多,我们一项一项来看。
VkInstance
在LessonClear中添加成员变量VkInstance vkIntance,在InitInstance()函数中初始化它。
1 unsafe class LessonClear { 2 VkInstance vkIntance; 3 bool isInitialized = false; 4 5 public void Init() { 6 if (this.isInitialized) { return; } 7 8 this.vkIntance = InitInstance(); 9 10 this.isInitialized = true;11 }12 13 private VkInstance InitInstance() {14 VkLayerProperties[] layerProperties;15 Layer.EnumerateInstanceLayerProperties(out layerProperties);16 string[] layersToEnable = layerProperties.Any(l => StringHelper.ToStringAnsi(l.LayerName) == "VK_LAYER_LUNARG_standard_validation")17 ? new[] { "VK_LAYER_LUNARG_standard_validation" }18 : new string[0];19 20 var appInfo = new VkApplicationInfo();21 {22 appInfo.SType = VkStructureType.ApplicationInfo;23 uint version = Vulkan.Version.Make(1, 0, 0);24 appInfo.ApiVersion = version;25 }26 27 var extensions = new string[] { "VK_KHR_surface", "VK_KHR_win32_surface", "VK_EXT_debug_report" };28 29 var info = new VkInstanceCreateInfo();30 {31 info.SType = VkStructureType.InstanceCreateInfo;32 extensions.Set(ref info.EnabledExtensionNames, ref info.EnabledExtensionCount);33 layersToEnable.Set(ref info.EnabledLayerNames, ref info.EnabledLayerCount);34 info.ApplicationInfo = (IntPtr)(&appInfo);35 }36 37 VkInstance result;38 VkInstance.Create(ref info, null, out result).Check();39 40 return result;41 }42 }
VkInstance的extension和layer是什么,一时难以说清,先不管。VkInstance像是一个缓存,它根据用户提供的参数,准备好了用户可能要用的东西。在创建VkInstance时,我明显感到程序卡顿了1秒。如果用户稍后请求的东西在缓存中,VkInstance就立即提供给他;如果不在,VkInstance就不给,并抛出VkResult。
以“Vk”开头的一般是Vulkan的结构体,或者对某种Vulkan对象的封装。
VkInstance就是一个对Vulkan对象的封装。创建一个VkInstance对象时,Vulkan的API只会返回一个 IntPtr 指针。在本库中,用一个class VkInstance将其封装起来,以便使用。
创建一个VkInstance对象时,需要我们提供给Vulkan API一个对应的 VkInstanceCreateInfo 结构体。这个结构体包含了创建VkInstance所需的各种信息,例如我们想让这个VkInstance支持哪些extension、哪些layer等。对于extension,显然,这必须用一个数组指针IntPtr和extension的总数来描述。
1 public struct VkInstanceCreateInfo { 2 public VkStructureType SType; 3 public IntPtr Next; 4 public UInt32 Flags; 5 public IntPtr ApplicationInfo; 6 public UInt32 EnabledLayerCount; 7 public IntPtr EnabledLayerNames; 8 public UInt32 EnabledExtensionCount; // 数组元素的数量 9 public IntPtr EnabledExtensionNames; // 数组指针10 }
这样的情况在Vulkan十分普遍,所以本库提供一个扩展方法来执行这一操作:
1 ///2 /// Set an array of structs to specified 6 /// 7 /// address of first element/array. 8 /// How many elements? 9 public static void Setand . 3 /// Enumeration types are not allowed to use this method. 4 /// If you have to, convert them to byte/short/ushort/int/uint according to their underlying types first. 5 ///(this T[] value, ref IntPtr target, ref UInt32 count) where T : struct {10 { // free unmanaged memory.11 if (target != IntPtr.Zero) {12 Marshal.FreeHGlobal(target);13 target = IntPtr.Zero;14 count = 0;15 }16 }17 {18 count = (UInt32)value.Length;19 20 int elementSize = Marshal.SizeOf ();21 int byteLength = (int)(count * elementSize);22 IntPtr array = Marshal.AllocHGlobal(byteLength);23 var dst = (byte*)array;24 GCHandle pin = GCHandle.Alloc(value, GCHandleType.Pinned);25 IntPtr address = Marshal.UnsafeAddrOfPinnedArrayElement(value, 0);26 var src = (byte*)address;27 for (int i = 0; i < byteLength; i++) {28 dst[i] = src[i];29 }30 pin.Free();31 32 target = array;33 }34 }
这个Set<T>()函数的核心作用是:在非托管内存上创建一个数组,将托管内存中的数组T[] value中的数据复制过去,然后,记录非托管内存中的数组的首地址(target)和元素数量(count)。当然,如果这不是第一次让target记录非托管内存中的某个数组,那就意味着首先应当将target指向的数组释放掉。
如果这里的T是枚举类型, Marshal.SizeOf() 会抛出异常,所以,必须先将枚举数组转换为 byte/short/ushort/int/uint 类型的数组。至于Marshal.SizeOf为什么会抛异常,我也不知道。
如果这里的T是string,那么必须用另一个变种函数代替:
1 ///2 /// Set an array of strings to specified 4 /// 5 /// address of first element/array. 6 /// How many elements? 7 public static void Set(this string[] value, ref IntPtr target, ref UInt32 count) { 8 { // free unmanaged memory. 9 var pointer = (IntPtr*)(target.ToPointer());10 if (pointer != null) {11 for (int i = 0; i < count; i++) {12 Marshal.FreeHGlobal(pointer[i]);13 }14 }15 }16 {17 int length = value.Length;18 if (length > 0) {19 int elementSize = Marshal.SizeOf(typeof(IntPtr));20 int byteLength = (int)(length * elementSize);21 IntPtr array = Marshal.AllocHGlobal(byteLength);22 IntPtr* pointer = (IntPtr*)array.ToPointer();23 for (int i = 0; i < length; i++) {24 IntPtr str = Marshal.StringToHGlobalAnsi(value[i]);25 pointer[i] = str;26 }27 target = array;28 }29 count = (UInt32)length;30 }31 }and . 3 ///
实现和解释起来略显复杂,但使用起来十分简单:
1 var extensions = new string[] { "VK_KHR_surface", "VK_KHR_win32_surface", "VK_EXT_debug_report" };2 extensions.Set(ref info.EnabledExtensionNames, ref info.EnabledExtensionCount);3 var layersToEnable = new[] { "VK_LAYER_LUNARG_standard_validation" };4 layersToEnable.Set(ref info.EnabledLayerNames, ref info.EnabledLayerCount);
在后续创建其他Vulkan对象时,我们将多次使用这一方法。
创建VkInstance的内部过程,就是调用Vulkan API的问题:
1 namespace Vulkan { 2 public unsafe partial class VkInstance : IDisposable { 3 public readonly IntPtr handle; 4 private readonly UnmanagedArraycallbacks; 5 6 public static VkResult Create(ref VkInstanceCreateInfo createInfo, UnmanagedArray callbacks, out VkInstance instance) { 7 VkResult result = VkResult.Success; 8 var handle = new IntPtr(); 9 VkAllocationCallbacks* pAllocator = callbacks != null ? (VkAllocationCallbacks*)callbacks.header : null;10 fixed (VkInstanceCreateInfo* pCreateInfo = &createInfo) {11 vkAPI.vkCreateInstance(pCreateInfo, pAllocator, &handle).Check();12 }13 14 instance = new VkInstance(callbacks, handle);15 16 return result;17 }18 19 private VkInstance(UnmanagedArray callbacks, IntPtr handle) {20 this.callbacks = callbacks;21 this.handle = handle;22 }23 24 public void Dispose() {25 VkAllocationCallbacks* pAllocator = callbacks != null ? (VkAllocationCallbacks*)callbacks.header : null;26 vkAPI.vkDestroyInstance(this.handle, pAllocator);27 }28 }29 30 class vkAPI {31 const string VulkanLibrary = "vulkan-1";32 33 [DllImport(VulkanLibrary, CallingConvention = CallingConvention.Winapi)]34 internal static unsafe extern VkResult vkCreateInstance(VkInstanceCreateInfo* pCreateInfo, VkAllocationCallbacks* pAllocator, IntPtr* pInstance);35 36 [DllImport(VulkanLibrary, CallingConvention = CallingConvention.Winapi)]37 internal static unsafe extern void vkDestroyInstance(IntPtr instance, VkAllocationCallbacks* pAllocator);38 }39 }
在 public static VkResult Create(ref VkInstanceCreateInfo createInfo, UnmanagedArray<VkAllocationCallbacks> callbacks, out VkInstance instance); 函数中:
第一个参数用ref标记,是因为这样就会强制程序员提供一个 VkInstanceCreateInfo 结构体。如果改用 VkInstanceCreateInfo* ,那么程序员就有可能提供一个null指针,这对于Vulkan API的 vkCreateInstance() 是没有应用意义的。
对第二个参数提供null指针是有应用意义的,但是,如果用 VkAllocationCallbacks* ,那么此参数指向的对象仍旧可能位于托管内存中(从而,在后续阶段,其位置有可能被GC改变)。用 UnmanagedArray<VkAllocationCallbacks> 就可以保证它位于非托管内存。
对于第三个参数,之所以让它用out标记(而不是放到返回值上),是因为 vkCreateInstance() 的返回值是 VkResult 。这样写,可以保持代码的风格与Vulkan一致。如果以后需要用切面编程之类的的方式添加log等功能,这样的一致性就会带来便利。
在函数中声明的结构体变量(例如这里的 var handle = new IntPtr(); ),可以直接取其地址( &handle )。
创建VkInstance的方式方法流程,与创建其他Vulkan对象的方式方法流程是极其相似的。读者可以触类旁通。
VkSurfaceKhr
在LessonClear中添加成员变量VkSurfaceKhr vkSurface,在InitSurface()函数中初始化它。
1 namespace Lesson01Clear { 2 unsafe class LessonClear { 3 VkInstance vkIntance; 4 VkSurfaceKhr vkSurface; 5 bool isInitialized = false; 6 7 public void Init(IntPtr hwnd, IntPtr processHandle) { 8 if (this.isInitialized) { return; } 9 10 this.vkIntance = InitInstance();11 this.vkSurface = InitSurface(this.vkIntance, hwnd, processHandle);12 13 this.isInitialized = true;14 }15 16 private VkSurfaceKhr InitSurface(VkInstance instance, IntPtr hwnd, IntPtr processHandle) {17 var info = new VkWin32SurfaceCreateInfoKhr {18 SType = VkStructureType.Win32SurfaceCreateInfoKhr,19 Hwnd = hwnd, // handle of User Control.20 Hinstance = processHandle, //Process.GetCurrentProcess().Handle21 };22 return instance.CreateWin32SurfaceKHR(ref info, null);23 }24 }25 }
可见,VkSurfaceKhr的创建与VkInstance遵循同样的模式,只是CreateInfo内容比较少。VkSurfaceKhr需要知道窗口句柄和进程句柄,这样它才能渲染到相应的窗口/控件上。
VkPhysicalDevice
这里的物理设备指的就是我们的计算机上的GPU了。
1 namespace Lesson01Clear { 2 unsafe class LessonClear { 3 VkInstance vkIntance; 4 VkSurfaceKhr vkSurface; 5 VkPhysicalDevice vkPhysicalDevice; 6 bool isInitialized = false; 7 8 public void Init(IntPtr hwnd, IntPtr processHandle) { 9 if (this.isInitialized) { return; }10 11 this.vkIntance = InitInstance();12 this.vkSurface = InitSurface(this.vkIntance, hwnd, processHandle);13 this.vkPhysicalDevice = InitPhysicalDevice();14 15 this.isInitialized = true;16 }17 18 private VkPhysicalDevice InitPhysicalDevice() {19 VkPhysicalDevice[] physicalDevices;20 this.vkIntance.EnumeratePhysicalDevices(out physicalDevices);21 return physicalDevices[0];22 }23 }24 }
创建VkPhysicalDivice对象不需要Callback:
1 namespace Vulkan { 2 public unsafe partial class VkPhysicalDevice { 3 public readonly IntPtr handle; 4 5 public static VkResult Enumerate(VkInstance instance, out VkPhysicalDevice[] physicalDevices) { 6 if (instance == null) { physicalDevices = null; return VkResult.Incomplete; } 7 8 UInt32 count; 9 VkResult result = vkAPI.vkEnumeratePhysicalDevices(instance.handle, &count, null).Check();10 var handles = stackalloc IntPtr[(int)count];11 if (count > 0) {12 result = vkAPI.vkEnumeratePhysicalDevices(instance.handle, &count, handles).Check();13 }14 15 physicalDevices = new VkPhysicalDevice[count];16 for (int i = 0; i < count; i++) {17 physicalDevices[i] = new VkPhysicalDevice(handles[i]);18 }19 20 return result;21 }22 23 private VkPhysicalDevice(IntPtr handle) {24 this.handle = handle;25 }26 }27 }
在函数中声明的变量(例如这里的 var handle = new IntPtr(); ),可以直接取其地址( &handle )。
但是在函数中声明的数组,数组本身是在堆中的,不能直接取其地址。为了能够取其地址,可以用( var handles = stackalloc IntPtr[(int)count]; )这样的方式,这会将数组本身创建到函数自己的栈空间,从而可以直接取其地址了。
VkDevice
这个设备是对物理设备的缓存\抽象\接口,我们想使用物理设备的哪些功能,就在CreateInfo中指定,然后创建VkDevice。(不指定的功能,以后就无法使用。)后续各种对象,都是用VkDevice创建的。
namespace Lesson01Clear { unsafe class LessonClear { VkInstance vkIntance; VkSurfaceKhr vkSurface; VkPhysicalDevice vkPhysicalDevice; VkDevice vkDevice; bool isInitialized = false; public void Init(IntPtr hwnd, IntPtr processHandle) { if (this.isInitialized) { return; } this.vkIntance = InitInstance(); this.vkSurface = InitSurface(this.vkIntance, hwnd, processHandle); this.vkPhysicalDevice = InitPhysicalDevice(); VkSurfaceFormatKhr surfaceFormat = SelectFormat(this.vkPhysicalDevice, this.vkSurface); VkSurfaceCapabilitiesKhr surfaceCapabilities; this.vkPhysicalDevice.GetSurfaceCapabilitiesKhr(this.vkSurface, out surfaceCapabilities); this.vkDevice = InitDevice(this.vkPhysicalDevice, this.vkSurface); this.isInitialized = true; } private VkDevice InitDevice(VkPhysicalDevice physicalDevice, VkSurfaceKhr surface) { VkQueueFamilyProperties[] properties = physicalDevice.GetQueueFamilyProperties(); uint index; for (index = 0; index < properties.Length; ++index) { VkBool32 supported; physicalDevice.GetSurfaceSupportKhr(index, surface, out supported); if (!supported) { continue; } if (properties[index].QueueFlags.HasFlag(VkQueueFlags.QueueGraphics)) break; } var queueInfo = new VkDeviceQueueCreateInfo(); { queueInfo.SType = VkStructureType.DeviceQueueCreateInfo; new float[] { 1.0f }.Set(ref queueInfo.QueuePriorities, ref queueInfo.QueueCount); queueInfo.QueueFamilyIndex = index; } var deviceInfo = new VkDeviceCreateInfo(); { deviceInfo.SType = VkStructureType.DeviceCreateInfo; new string[] { "VK_KHR_swapchain" }.Set(ref deviceInfo.EnabledExtensionNames, ref deviceInfo.EnabledExtensionCount); new VkDeviceQueueCreateInfo[] { queueInfo }.Set(ref deviceInfo.QueueCreateInfos, ref deviceInfo.QueueCreateInfoCount); } VkDevice device; physicalDevice.CreateDevice(ref deviceInfo, null, out device); return device; } }}
后续的Queue、Swapchain、Image、RenderPass、Framebuffer、Fence和Semaphore等都不再一一介绍,毕竟都是十分类似的创建过程。
最后只介绍一下VkCommandBuffer。
VkCommandBuffer
Vulkan可以将很多渲染指令保存到buffer,将buffer一次性上传到GPU内存,这样以后每次调用它即可,不必重复提交这些数据了。
1 namespace Lesson01Clear { 2 unsafe class LessonClear { 3 VkInstance vkIntance; 4 VkSurfaceKhr vkSurface; 5 VkPhysicalDevice vkPhysicalDevice; 6 7 VkDevice vkDevice; 8 VkQueue vkQueue; 9 VkSwapchainKhr vkSwapchain;10 VkImage[] vkImages;11 VkRenderPass vkRenderPass;12 VkFramebuffer[] vkFramebuffers;13 VkFence vkFence;14 VkSemaphore vkSemaphore;15 VkCommandBuffer[] vkCommandBuffers;16 bool isInitialized = false;17 18 public void Init(IntPtr hwnd, IntPtr processHandle) {19 if (this.isInitialized) { return; }20 21 this.vkIntance = InitInstance();22 this.vkSurface = InitSurface(this.vkIntance, hwnd, processHandle);23 this.vkPhysicalDevice = InitPhysicalDevice();24 VkSurfaceFormatKhr surfaceFormat = SelectFormat(this.vkPhysicalDevice, this.vkSurface);25 VkSurfaceCapabilitiesKhr surfaceCapabilities;26 this.vkPhysicalDevice.GetSurfaceCapabilitiesKhr(this.vkSurface, out surfaceCapabilities);27 28 this.vkDevice = InitDevice(this.vkPhysicalDevice, this.vkSurface);29 30 this.vkQueue = this.vkDevice.GetDeviceQueue(0, 0);31 this.vkSwapchain = CreateSwapchain(this.vkDevice, this.vkSurface, surfaceFormat, surfaceCapabilities);32 this.vkImages = this.vkDevice.GetSwapchainImagesKHR(this.vkSwapchain);33 this.vkRenderPass = CreateRenderPass(this.vkDevice, surfaceFormat);34 this.vkFramebuffers = CreateFramebuffers(this.vkDevice, this.vkImages, surfaceFormat, this.vkRenderPass, surfaceCapabilities);35 36 var fenceInfo = new VkFenceCreateInfo() { SType = VkStructureType.FenceCreateInfo };37 this.vkFence = this.vkDevice.CreateFence(ref fenceInfo);38 var semaphoreInfo = new VkSemaphoreCreateInfo() { SType = VkStructureType.SemaphoreCreateInfo };39 this.vkSemaphore = this.vkDevice.CreateSemaphore(ref semaphoreInfo);40 41 this.vkCommandBuffers = CreateCommandBuffers(this.vkDevice, this.vkImages, this.vkFramebuffers, this.vkRenderPass, surfaceCapabilities);42 43 this.isInitialized = true;44 }45 46 VkCommandBuffer[] CreateCommandBuffers(VkDevice device, VkImage[] images, VkFramebuffer[] framebuffers, VkRenderPass renderPass, VkSurfaceCapabilitiesKhr surfaceCapabilities) {47 var createPoolInfo = new VkCommandPoolCreateInfo {48 SType = VkStructureType.CommandPoolCreateInfo,49 Flags = VkCommandPoolCreateFlags.ResetCommandBuffer50 };51 var commandPool = device.CreateCommandPool(ref createPoolInfo);52 var commandBufferAllocateInfo = new VkCommandBufferAllocateInfo {53 SType = VkStructureType.CommandBufferAllocateInfo,54 Level = VkCommandBufferLevel.Primary,55 CommandPool = commandPool.handle,56 CommandBufferCount = (uint)images.Length57 };58 VkCommandBuffer[] buffers = device.AllocateCommandBuffers(ref commandBufferAllocateInfo);59 for (int i = 0; i < images.Length; i++) {60 61 var commandBufferBeginInfo = new VkCommandBufferBeginInfo() {62 SType = VkStructureType.CommandBufferBeginInfo63 };64 buffers[i].Begin(ref commandBufferBeginInfo);65 {66 var renderPassBeginInfo = new VkRenderPassBeginInfo();67 {68 renderPassBeginInfo.SType = VkStructureType.RenderPassBeginInfo;69 renderPassBeginInfo.Framebuffer = framebuffers[i].handle;70 renderPassBeginInfo.RenderPass = renderPass.handle;71 new VkClearValue[] { new VkClearValue { Color = new VkClearColorValue(0.9f, 0.7f, 0.0f, 1.0f) } }.Set(ref renderPassBeginInfo.ClearValues, ref renderPassBeginInfo.ClearValueCount);72 renderPassBeginInfo.RenderArea = new VkRect2D {73 Extent = surfaceCapabilities.CurrentExtent74 };75 };76 buffers[i].CmdBeginRenderPass(ref renderPassBeginInfo, VkSubpassContents.Inline);77 {78 // nothing to do in this lesson.79 }80 buffers[i].CmdEndRenderPass();81 }82 buffers[i].End();83 }84 return buffers;85 }86 }87 }
本例中的VkClearValue用于指定背景色,这里指定了黄色,运行效果如下:
总结
如果看不懂本文,就去看代码,运行代码,再来看本文。反反复复看,总会懂。