Vulkan(0)搭建环境-清空窗口
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。
首先,在官网(https://vulkan.lunarg.com)下载vulkan-sdk.exe和vulkan-runtime.exe。完后安装。vulkan-runtime.exe也可以在(https://files.cnblogs.com/files/bitzhuwei/vulkan-runtime.rar)下载。vulkan-sdk.exe太大,我就不提供下载了。
然后,下载Vulkan.net库(https://github.com/bitzhuwei/Vulkan.net)。这是本人搜罗整理来的一个Vulkan库,外加一些示例代码。用VS2017打开Vulkan.sln,在这个解决方案下就可以学习使用Vulkan了。
如果读者在Github上的下载速度太慢,可以试试将各个文件单独点开下载。这很笨,但也是个办法。
简单介绍下此解决方案。
Vulkan文件夹下的Vulkan.csproj是对Vulkan API的封装。Vulkan使用了大量的struct、enum,这与OpenGL类似。
Vulkan.Platforms文件夹下的Vulkan.Platforms.csproj是平台相关的一些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是枚举类型, Marshal.SizeOf() 会抛出异常,所以,必须先将枚举数组转换为 byte/short/ushort/int/uint 类型的数组。至于Marshal.SizeOf为什么会抛异常,我也不知道。
如果这里的T是string,那么必须用另一个变种函数代替:
1 ///public static void Set(this string[] value, ref IntPtr target, ref UInt32 count)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 }
实现和解释起来略显复杂,但使用起来十分简单:
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
第一个参数用ref标记,是因为这样就会强制程序员提供一个 VkInstanceCreateInfo 结构体。如果改用 VkInstanceCreateInfo* ,那么程序员就有可能提供一个null指针,这对于Vulkan API的 vkCreateInstance() 是没有应用意义的。
对第二个参数提供null指针是有应用意义的,但是,如果用 VkAllocationCallbacks* ,那么此参数指向的对象仍旧可能位于托管内存中(从而,在后续阶段,其位置有可能被GC改变)。用 UnmanagedArray
对于第三个参数,之所以让它用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().Handle 21 }; 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.ResetCommandBuffer 50 }; 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.Length 57 }; 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.CommandBufferBeginInfo 63 }; 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.CurrentExtent 74 }; 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用于指定背景色,这里指定了黄色,运行效果如下:
总结
如果看不懂本文,就去看代码,运行代码,再来看本文。反反复复看,总会懂。