博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Vulkan(0)搭建环境-清空窗口
阅读量:5127 次
发布时间:2019-06-13

本文共 21426 字,大约阅读时间需要 71 分钟。

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 
and
. 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 ///
6 /// 7 /// address of first element/array. 8 /// How many elements? 9 public static void Set
(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 
and
. 3 ///
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 }
public static void Set(this string[] value, ref IntPtr target, ref UInt32 count)

 

实现和解释起来略显复杂,但使用起来十分简单:

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 UnmanagedArray
callbacks; 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用于指定背景色,这里指定了黄色,运行效果如下:

 

 

总结

如果看不懂本文,就去看代码,运行代码,再来看本文。反反复复看,总会懂。

 

转载于:https://www.cnblogs.com/bitzhuwei/p/Vulkan-0-Environment-Clear-Window.html

你可能感兴趣的文章
洛谷P1099 树网的核
查看>>
Spring Cloud 入门教程(八): 断路器指标数据监控Hystrix Dashboard 和 Turbine
查看>>
阅读思考作业2
查看>>
理解 OpenStack 高可用(HA) (4): Pacemaker 和 OpenStack Resource Agent (RA)
查看>>
STL (2)List
查看>>
Linux编译错误集
查看>>
【HBuilder】手机App推送至Apple App Store过程
查看>>
2014.12.14 python&pip
查看>>
python框架之Django(16)-接入Redis
查看>>
Vue声明式和编程式导航
查看>>
死锁的四个必要条件以及怎样处理
查看>>
用商业模式改变世界(上)
查看>>
Xiph开源项目的相关问题-VORBIS音频解析
查看>>
PHP 布尔类型
查看>>
【模板】线段树 主席树
查看>>
例2-5+2-6
查看>>
作业5
查看>>
PHP定时执行任务的3种方法详解
查看>>
AD16 快速原理图封装导出
查看>>
CentOS 6 安装HBase集群教程
查看>>