vulkanAPI学习笔记(一)
来源:互联网 发布:华视身份证阅读器软件 编辑:程序博客网 时间:2024/06/16 08:14
Gobal Tips:
0.此为本人学习Vulkan Programming Guide的笔记,所学习的书本翻译资料来自Blary的专栏和know yourself,十分感谢他们
1.函数中标记为[IN]的是输入值,标记为[OUT]的是返回值,
2.本人看不懂或者是不清楚的均不作介绍,以免误导他人,但日后会尽力补齐
3.本文面向包括作者在内的所有菜鸟级的开发者,所以所有的Demo代码均不精简,为的是读者能够直接复制使用减少出错
4.因为是笔记所以免不了摘抄,但本人都尽量按照自己的理解和实践去重新总结归纳过了,无意侵犯他人,如有,还请理解原谅
摘要
Vulkan包括一个层次结构的功能,如下图所示
Vulkan实例:
是一个软件构造,在逻辑上将应用程序的状态与在应用程序上下文中运行的其他应用程序或库分离。系统中的物理设备被呈现为实例的成员,每个实例具有某些能力,包括可用队列的选择。
物理设备:
通俗来讲就是你电脑的显卡
逻辑设备:
由Vulkan实例创建的逻辑设备是围绕物理设备的软件结构,并且表示与特定物理设备相关联的资源的预留。
这包括物理设备上可用队列的可能子集。可以在一个物理设备上创建的多个逻辑设备,您的应用程序将花费大部分时间与逻辑设备进行交互。
创建[vulkan实例]
VkResult vkCreateInstance (const VkInstanceCreateInfo* pCreateInfo,//[IN]传入构造信息const VkAllocationCallbacks* pAllocator,//[IN]传入自制内存分配器 //若设置为nullptr,则使用vulkan内置的内存分配器VkInstance* pInstance//[OUT]返回生成的vulkan实例);//构造[vulkan实例]的信息结构体typedef struct VkInstanceCreateInfo { VkStructureType sType;//设为VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO//几乎每个Vulkan结构中用于向API传递参数的第一个成员是sType字段,它告诉Vulkan这是什么类型的结构。//sType字段均有一个约定俗成的套路,可以对照类名给sType赋值
const void* pNext;//设为nullptrVkInstanceCreateFlags flags;//设为0const VkApplicationInfo* pApplicationInfo;//用于放置用于描述应用程序信息的结构体(的地址) //可以将它设置为nullptr(默认值)uint32_t enabledLayerCount;//要启用的实例层数及其名称的个数。//图层用于拦截Vulkan API,并提供日志记录,概要分析,调试或其他附加功能。//如果不需要图层,只需将enabledLayerCount设置为零,并将ppEnabledLayerNames保留为nullptr。const char* const* ppEnabledLayerNames;//图层名字字符串的数组uint32_t enabledExtensionCount;//要启用的扩展数的计数//若不使用则设为0,ppEnabledExtensionNames设为nullptr
const char* const* ppEnabledExtensionNames;//对应名字字符串的数组} VkInstanceCreateInfo;//由于名字数组都是const char* const*的不能改变值也不能改变地址//所以要求使用者在创建信息结构体实例时就对其进行列表初始化 //用于描述应用程序信息的结构体typedef struct VkApplicationInfo { VkStructureType sType;//设为 VK_STRUCTURE_TYPE_APPLICATION_INFOconst void* pNext;//设为nullptrconst char* pApplicationName;//该字符串包含您应用程序的名称uint32_t applicationVersion;//该字符串包含您应用程序的版本const char* pEngineName;//该字符串包含您引擎的名称uint32_t engineVersion;//该字符串包含您引擎的版本uint32_t apiVersion;//建议设为0} VkApplicationInfo;
[物理设备]
vkCreateInstance()函数成功,我们可以使用它来找到系统中安装的Vulkan兼容设备
但在我们可以创建[逻辑设备]之前,我们必须找到[物理设备]。
//调用vkEnumeratePhysicalDevices()函数来找到系统中安装的Vulkan兼容设备VkResult vkEnumeratePhysicalDevices ( VkInstance instance,//[IN]传入[实例] uint32_t* pPhysicalDeviceCount,//[OUT]返回支持的设备数 VkPhysicalDevice* pPhysicalDevices//[OUT]返回支持的[物理设备]的数组 );//这个函数希望你调用它两次,//第一次只需要将pPhysicalDevices设置为nullptr(尽管pPhysicalDeviceCount仍然必须是有效的指针)//获取pPhysicalDeviceCount的返回值,用来调整数组大小//并且第二次把数组传入pPhysicalDevices,便可获得支持的[物理设备]的列表
//物理设备句柄用于查询设备的功能,并最终创建逻辑设备void vkGetPhysicalDeviceProperties (
VkPhysicalDevice physicalDevice,//[IN]传入物理设备 VkPhysicalDeviceProperties* pProperties//[OUT]返回其属性 ); //[物理设备]的属性结构体typedef struct VkPhysicalDeviceProperties { uint32_t apiVersion;//设备支持的最高版本的Vulkanuint32_t driverVersion;//包含用于控制设备的驱动程序的版本uint32_t vendorID;uint32_t deviceID;VkPhysicalDeviceType deviceType;char deviceName[VK_MAX_PHYSICAL_DEVICE_NAME_SIZE];//设备名,比如我的是 GeForce 940MXuint8_t pipelineCacheUUID[VK_UUID_SIZE];VkPhysicalDeviceLimits limits;VkPhysicalDeviceSparseProperties sparseProperties;} VkPhysicalDeviceProperties;
Demo:创建vulkan实例并查询[物理设备]
//Demo.cpp//本demo假设读者充分了解标准库的vector模板,若您不了解,作者推荐您先去查阅C++primer(不带plus)这本书#pragma warning(disable:4996)#include <vulkan/vulkan.hpp>#include <vulkan/vk_sdk_platform.h>#include <iostream>#include <conio.h>#include <vector>int main(){ VkInstance m_instance;//新建以备用 VkResult result = VK_SUCCESS; VkApplicationInfo appInfo = {};//新建以备用 VkInstanceCreateInfo instanceCreateInfo = {};//新建以备用 appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; appInfo.pApplicationName = "Application"; appInfo.applicationVersion = 1; appInfo.apiVersion = VK_MAKE_VERSION(1, 0, 0); instanceCreateInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; instanceCreateInfo.pApplicationInfo = &appInfo; result = vkCreateInstance(&instanceCreateInfo, nullptr, &m_instance); //创建VULKAN实例 if (result == VK_SUCCESS) { uint32_t physicalDeviceCount = 0;//新建以备用 result = vkEnumeratePhysicalDevices(m_instance, &physicalDeviceCount, nullptr); //获取[物理设备]数组的大小 std::vector<VkPhysicalDevice>m_physicalDevices; //保存[物理设备]的数组 if (result == VK_SUCCESS) { m_physicalDevices.resize(physicalDeviceCount);//重定数组大小 vkEnumeratePhysicalDevices(m_instance, &physicalDeviceCount, &m_physicalDevices[0]);//获取[物理设备]数组 VkPhysicalDeviceProperties m_deviceProperties; vkGetPhysicalDeviceProperties(m_physicalDevices[0], &m_deviceProperties); std::cout << m_deviceProperties.apiVersion << "\n"//打印设备信息 << m_deviceProperties.deviceID << "\n" << m_deviceProperties.deviceName << "\n" << m_deviceProperties.deviceType << "\n" << m_deviceProperties.driverVersion << std::endl;//...... } } getch(); return result == VK_SUCCESS ? 1 : 0;}
[物理设备]的内存
通俗讲就是你显卡的显存.
可用作纹理和其他数据的后备存储.
分为多种类型,每种类型都有一组属性.
每种类型的存储器然后由设备的堆之一支持,可能会有几个.
//[物理设备]内存属性查询函数void vkGetPhysicalDeviceMemoryProperties (
VkPhysicalDevice physicalDevice, //[IN]传入[物理设备] VkPhysicalDeviceMemoryProperties* pMemoryProperties //[OUT]返回[物理设备]属性 );//[物理设备]内存属性结构体typedef struct VkPhysicalDeviceMemoryProperties { uint32_t memoryTypeCount;//内存类型数,最大值为VK_MAX_MEMORY_TYPES=32
VkMemoryType memoryTypes[VK_MAX_MEMORY_TYPES];//每种内存类型的属性的数组 uint32_t memoryHeapCount; //可用的堆数量,最大值为VK_MAX_MEMORY_HEAPS=16VkMemoryHeap memoryHeaps[VK_MAX_MEMORY_HEAPS];//堆的属性数组} VkPhysicalDeviceMemoryProperties; //内存类型属性结构体typedef struct VkMemoryType { VkMemoryPropertyFlags propertyFlags; //描述了内存的类型//通常flag都存储着一些位,位描述了所在结构的具体属性,这个字段将在下文介绍uint32_t heapIndex; //此种内存类型对应的堆的下标} VkMemoryType; //堆的属性的结构体typedef struct VkMemoryHeap { VkDeviceSize size; //堆大小,以byte为单位VkMemoryHeapFlags flags;//无需关心 } VkMemoryHeap;
VkMemoryType结构体的propertyFlags字段由这些位构成:
•VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT 表示内存是本地的(即物理连接到)设备。如果该位未设置,则可以假定存储器对于主机是本地的。
•VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT 表示使用的内存分配
此类型可以由主机映射和读取或写入。如果该位未被设置,则这种类型的存储器不能被主机直接访问,而是被设备独占使用。
•VK_MEMORY_PROPERTY_HOST_COHERENT_BIT 表示当此类型的内存由主机和设备同时访问时,这些访问将在两个客户端之间一致。如果未设置此位,则设备或主机可能不会看到每个执行的写入的结果,直到高速缓存被显式刷新。
•VK_MEMORY_PROPERTY_HOST_CACHED_BIT 表示此类型的内存中的数据由主机缓存。对这种类型的存储器的读取访问通常比如果该位未被设置时的读取访问更快。然而,设备的访问可以具有略高的延迟,特别是如果存储器也是一致的。
•VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT 表示使用此类型分配的内存不一定立即从关联的堆消耗空间,并且驱动程序可能推迟物理内存分配,直到内存对象用于回退资源。
[物理设备]的[队列系列]
Vulkan设备执行提交到队列的工作。
每个设备将有一个或多个队列,并且每个队列将属于设备的队列系列之一。
[队列系列]是一组具有相同功能但能够并行运行的队列。
队列系列的数量,每个系列的能力以及属于每个系列的队列的数量都是物理设备的属性。
//[物理设备]的[队列系列]查询函数void vkGetPhysicalDeviceQueueFamilyProperties ( VkPhysicalDevice physicalDevice,//[IN]传入[物理设备]uint32_t* pQueueFamilyPropertyCount,//[OUT]返回[物理设备]数VkQueueFamilyProperties* pQueueFamilyProperties);//[OUT]返回[物理设备]的[队列系列]的数组//这个函数希望你调用它两次,具体用法与vkEnumeratePhysicalDevices()类似//通过第一次调用//vkGetPhysicalDeviceQueueFamilyProperties(m_device,&m_queueFamilyPropertyCount,nullptr);//获取并确定自己[物理设备]的[队列系列]的数量m_queueFamilyPropertyCount//在第二次调用时,在pQueueFamilyProperties中传递此数组,Vulkan将返回[物理设备]的[队列系列]的属性。
[物理设备]的[队列系列]的属性
//vkGetPhysicalDeviceQueueFamilyProperties()的返回值 typedef struct VkQueueFamilyProperties { VkQueueFlags queueFlags; //用于描述队列的总体功能uint32_t queueCount; //队列数量uint32_t timestampValidBits;//指示当从队列获取时间戳时有多少位有效VkExtent3D minImageTransferGranularity;//指定队列支持图像传输的单元(如果有的话)} VkQueueFamilyProperties;
字段queueFlags所表示的队列的总体功能,由VkQueueFlagBits位的组合构成所描述
•如果设置了VK_QUEUE_GRAPHICS_BIT,则此系列中的队列支持图形操作,例如绘制点,线和三角形。
•如果设置了VK_QUEUE_COMPUTE_BIT,则此系列中的队列支持计算操作,例如调度计算着色器。
•如果设置了VK_QUEUE_TRANSFER_BIT,则此系列中的队列支持传输操作,例如复制缓冲区和映像内容。
•如果设置了VK_QUEUE_SPARSE_BINDING_BIT,则此系列中的队列支持用于更新稀疏资源的内存绑定操作。
timestampValidBits字段:暂暂时不作介绍,待日后补齐
minImageTimestampGranularity字段:暂时不作介绍,待日后补齐
Demo:查询[物理设备]的属性
//接上文Demo.cpp的......处 VkPhysicalDeviceMemoryProperties m_physicalDeviceMemoryProperties; //新建以备用 uint32_t queueFamilyPropertyCount;//新建以备用 std::vector<VkQueueFamilyProperties> queueFamilyProperties; //保存[物理设备]的[队列系列]的数组 vkGetPhysicalDeviceMemoryProperties(m_physicalDevices[0], &m_physicalDeviceMemoryProperties); //查询[物理设备]的内存属性 vkGetPhysicalDeviceQueueFamilyProperties(m_physicalDevices[0], &queueFamilyPropertyCount, nullptr); //获取[物理设备]的[队列系列]数 queueFamilyProperties.resize(queueFamilyPropertyCount); vkGetPhysicalDeviceQueueFamilyProperties(m_physicalDevices[0], &queueFamilyPropertyCount, queueFamilyProperties.data()); //获取[物理设备]的[队列系列]//......
创建[物理设备]的[逻辑设备]
//[逻辑设备]创建函数VkResult vkCreateDevice (VkPhysicalDevice physicalDevice,//[IN]传入[物理设备]const VkDeviceCreateInfo* pCreateInfo, //[IN]传入构造信息//若设置为nullptr,则使用vulkan内置的内存分配器VkDevice* pDevice//[OUT]返回[逻辑设备]);
[逻辑设备]的构造信息
//[逻辑设备]的构造信息typedef struct VkDeviceCreateInfo { VkStructureType sType;//和往常一样,取类名关键字设为 VK_STRUCTURE_TYPE_DEVICE_CREATE_INFOconst void* pNext;//设为nullptr VkDeviceCreateFlags flags;//设为0uint32_t queueCreateInfoCount;//与pQueueCreateInfos字段相关,队列数量const VkDeviceQueueCreateInfo* pQueueCreateInfos;//[逻辑设备]的队列的构造信息的数组uint32_t enabledLayerCount;//暂时不作介绍,先设为0const char* const* ppEnabledLayerNames;//暂时不作介绍,先设为nullptruint32_t enabledExtensionCount;//暂时不作介绍,先设为0const char* const* ppEnabledExtensionNames;//暂时不作介绍,先设为nullptrconst VkPhysicalDeviceFeatures* pEnabledFeatures;//指定您的应用程序希望使用哪些可选功能。//如果你不想使用任何可选功能,则设置为nullptr。} VkDeviceCreateInfo; //[逻辑设备]的队列的构造信息typedef struct VkDeviceQueueCreateInfo { VkStructureType sType;//和往常一样,取类名关键字设为 VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFOconst void* pNext; //设为nullptr VkDeviceQueueCreateFlags flags; //设为0uint32_t queueFamilyIndex; //指定要创建在哪个[队列系列]中的[队列系列]下标//下标可以通过调用vkGetPhysicalDeviceQueueFamilyProperties()得到uint32_t queueCount; //将queueCount设置为要使用的队列数。//请确保设备必须至少在你选择的[队列系列]中有这么多的队列const float* pQueuePriorities;//队列优先级数组,范围0-1.0,值高则调用更加积极.//数组大小应该与queueCount字段值相同,设置为nullptr会使队列保持相同的默认优先级} VkDeviceQueueCreateInfo;
//要确定设备支持哪些可选功能,请调用vkGetPhysicalDeviceFeatures() void vkGetPhysicalDeviceFeatures( VkPhysicalDevice physicalDevice,//[IN]传入物理设备 VkPhysicalDeviceFeatures* pFeatures//[OUT]返回设备特点的结构体 );//在此列举vulkan1.0.46.0支持的部分设备特点,可以再头文件中查看这些特点,支持与否是一个bool值typedef struct VkPhysicalDeviceFeatures { VkBool32 robustBufferAccess; VkBool32 fullDrawIndexUint32; VkBool32 imageCubeArray; VkBool32 independentBlend; VkBool32 geometryShader; VkBool32 tessellationShader;//...... VkBool32 sparseResidency8Samples; VkBool32 sparseResidency16Samples; VkBool32 sparseResidencyAliased; VkBool32 variableMultisampleRate; VkBool32 inheritedQueries;} VkPhysicalDeviceFeatures; //查询完成后,你可以将哪个VkPhysicalDeviceFeatures结构传回vkCreateDevice()//这样你就可以启用每个可选功能设备支持并且不请求设备不支持的特征。
Demo:创建逻辑设备并启用曲面细分(需要设备支持)
//接上文Demo.cpp的......处VkPhysicalDeviceFeatures supportedFeatures;//被支持的特点VkPhysicalDeviceFeatures requiredFeatures = {};//我们想使用的特点,这里注意:因为typedef uint32_t VkBool32;所以VkBool32初始化时是0(不支持);vkGetPhysicalDeviceFeatures(m_physicalDevices[0],&supportedFeatures);requiredFeatures.multiDrawIndirect = supportedFeatures.multiDrawIndirect;requiredFeatures.tessellationShader = VK_TRUE;requiredFeatures.geometryShader = VK_TRUE;const VkDeviceQueueCreateInfo deviceQueueCreateInfo = { VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, // sType nullptr, // pNext 0, // flags 0, // queueFamilyIndex 1, // queueCount nullptr // pQueuePriorities };const VkDeviceCreateInfo deviceCreateInfo = { VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, // sType nullptr, // pNext 0, //flags 1, // queueCreateInfoCount &deviceQueueCreateInfo, // pQueueCreateInfos 0, // enabledLayerCount nullptr, // ppEnabledLayerNames 0, // enabledExtensionCount nullptr, // ppEnabledExtensionNames &requiredFeatures // pEnabledFeatures }; VkDevice m_logicalDevice;//新建以备用 result = vkCreateDevice(m_physicalDevices[0], &deviceCreateInfo, nullptr, &m_logicalDevice);//......
对象类型和函数约定
Tips:事实上,Vulkan中的所有内容都被表示为一个由句柄引用的对象。
不管是我们创建的[vulkan实例]还是获取到的[物理设备]的实例其实都是它们的句柄,可以把句柄看做智能指针
vulkan中的句柄分为两大类:
–1.可分派对象:
是内部包含调度表的对象。
这是各种组件使用的功能表,用于确定当您的应用程序调用Vulkan时要执行的代码部分。
这些类型的对象通常是较大较复杂的结构,目前由:
实例(VkInstance),
物理设备(VkPhysicalDevice),
逻辑设备(VkDevice),
命令缓冲区(VkCommandBuffer),
队列(VkQueue)组成)
构成。
–2.不可分散对象:
所有其他对象被认为是不可分散的。
任何Vulkan函数的第一个参数总是一个可调度的对象。
此规则的唯一例外是与实例的创建和初始化相关的功能。
在大多数情况下,这些对象与应用程序无关,仅影响API的结构以及系统级组件(如Vulkan加载器和层)与这些对象的互操作性。
Vulkan的层
层是Vulkan的特征,允许修改其行为。
层通常拦截全部或部分Vulkan,并添加诸如日志记录,跟踪,提供诊断,分析等功能。
可以在实例级别添加一个层,在这种情况下,它会影响整个Vulkan实例以及可能由其创建的每个设备。
或者,可以在设备级别添加该层,在这种情况下,该层仅影响其启用的设备。
//[vulkan实例]层查询函数VkResult vkEnumerateInstanceLayerProperties (uint32_t* pPropertyCount, //[OUT]返回[vulkan实例]层的数量VkLayerProperties* pProperties//[OUT]返回[vulkan实例]层属性的数组);//这个函数希望你调用它两次//第一次把pProperties设为nullptr,确定层的数量,并调整数组大小//的二次再把数组传入//层的属性结构体typedef struct VkLayerProperties {
char layerName[VK_MAX_EXTENSION_NAME_SIZE]; //层名字符串uint32_t specVersion;
//规格版本uint32_t implementationVersion;
//实现版本char description[VK_MAX_DESCRIPTION_SIZE]; //此用于在用户界面中记录或显示} VkLayerProperties;//应用程序写入器可以识别层的特定实现,并且仅当该实现的版本超过特定版本时选择使用它.//例如用于避免已经确认的关键错误。
Demo:查询vulkan的层
//接上文Demo.cpp的......处//查询[vulkan实例]级的层 uint32_t numInstanceLayers = 0; std::vector<VkLayerProperties> instanceLayerProperties; vkEnumerateInstanceLayerProperties( &numInstanceLayers, nullptr); //如果有层存在 if (numInstanceLayers != 0) { instanceLayerProperties.resize(numInstanceLayers); vkEnumerateInstanceLayerProperties(&numInstanceLayers, instanceLayerProperties.data()); }//......
要启用实例级别的层,请将其名称先包含在用于创建实例的VkInstanceCreateInfo结构的ppEnabledLayerNames字段中,再对vkCreateInstance()进行调用
不仅在实例级别,层可以获取到层。 层也可以在设备级应用(实际上是用在[物理设备]的[逻辑设备]上)
//[物理]设备层查询函数VkResult vkEnumerateDeviceLayerProperties ( VkPhysicalDevice physicalDevice,//[IN]传入物理设备 uint32_t* pPropertyCount,//[OUT]返回层数 VkLayerProperties* pProperties//[OUT]返回层数组 //同样由VkLayerProperties结构的实例描述);
//接上文Demo.cpp的......处//查询[物理设备]的层 uint32_t numDeviceLayers = 0; std::vector<VkLayerProperties> deviceLayerProperties; vkEnumerateDeviceLayerProperties(m_physicalDevices[0], &numDeviceLayers, nullptr); if (numDeviceLayers != 0) { std::cout << numDeviceLayers << std::endl;//打印层数 deviceLayerProperties.resize(numDeviceLayers); vkEnumerateDeviceLayerProperties(m_physicalDevices[0],&numDeviceLayers, deviceLayerProperties.data()); }//......
为了在创建与系统中的[物理设备]相对应的[逻辑设备]时启用层,请在用于创建设备的VkDeviceCreateInfo的ppEnabledLayerNames成员中包含层名称,再对vkCreateDevice()进行调用
SDK中包含的层次:
•VK_LAYER_LUNARG_api_dump将Vulkan调用及其参数和值打印到控制台。
•VK_LAYER_LUNARG_core_validation对描述符集,流水线状态和动态状态中使用的参数和状态执行验证;验证SPIR-V模块和图形管道之间的接口;并跟踪并验证用于返回对象的GPU内存的使用情况。
•VK_LAYER_LUNARG_device_limits确保将值传递给Vulkan命令作为参数或数据结构成员属于设备支持的功能集限制。
•VK_LAYER_LUNARG_image验证图像使用情况与支持的格式一致。 •VK_LAYER_LUNARG_object_tracker对Vulkan对象执行跟踪,尝试执行
捕获泄漏,使用后自由错误和其他无效对象使用。
•VK_LAYER_LUNARG_parameter_validation确认传递给Vulkan函数的所有参数值都有效。
•VK_LAYER_LUNARG_swapchain对第5章“演示文稿”中描述的WSI(Windows系统集成)扩展功能提供的功能进行验证。
•VK_LAYER_GOOGLE_threading确保了对于线程的Vulkan命令的有效使用,确保没有两个线程在不同时间访问同一个对象。
•VK_LAYER_GOOGLE_unique_objects确保每个对象都有一个唯一的句柄,以便应用程序更容易的跟踪,避免实现可能会重复使用相同参数表示对象的句柄。
VK_LAYER_LUNARG_standard_validation:暂不作介绍,待日后补充
Vulkan扩展
暂不作介绍,待日后补充
- vulkanAPI学习笔记(一)
- vulkanAPI学习笔记(零)
- XSLT学习笔记(一)
- Castor学习笔记(一)
- CSS学习笔记一
- 汇编学习笔记(一)
- ArcXML学习笔记(一)
- C#学习笔记(一)
- AIX学习笔记(一)
- PHP学习笔记(一)
- 串口学习笔记(一)
- JetSpeed学习笔记(一)
- symbian学习笔记一
- JSP学习笔记一
- Eclipse学习笔记一
- Cryptopp学习笔记(一)
- J2EE 学习笔记 一
- SOA学习笔记一
- android fragment的简单使用
- Java编写由*构成的菱形
- mysql支持在同一个事务中出错而回滚多个数据库的数据吗?
- demo集合(一)
- IONIC2-查看IONIC2自带的icons图标
- vulkanAPI学习笔记(一)
- Tomacat7启动报错-org.apache.catalina.deploy.WebXml addFilter
- eclipse创建maven项目
- 基于OpenStack的虚拟机在线迁移
- Win7系统的快捷键大全 Win7键盘快捷键汇总
- Django本地配置ckeditor(windows系统)
- 【React Native】从源码一步一步解析它的实现原理
- SQLSERVER链接外部数据库服务器
- jxls 多个sheet 生成