-
Notifications
You must be signed in to change notification settings - Fork 90
/
Vulkan编程指南.tex
8731 lines (6128 loc) · 511 KB
/
Vulkan编程指南.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
\documentclass{ctexart}
\usepackage{hyperref}
\hypersetup{
colorlinks=true,
linkcolor=black,
filecolor=blue,
urlcolor=blue,
citecolor=cyan,
}
\usepackage{amsmath}
\usepackage{graphicx}
\usepackage{float}
\usepackage{listings}
\usepackage{xcolor}
\title{Vulkan编程指南}
\author{Alexander Overvoorde著\\fangcun译}
\begin{document}
\maketitle
\newpage
\setcounter{tocdepth}{2}
\tableofcontents
\newpage
\section{序}
下文来自仓颉输入法的发明人朱邦复先生,放在这里的原因是本人认为Vulkan之于汇编有着相似的地位。
一、结构基础
物质文明之有今天的成就,是因为人类掌握了物质的基本结构。物质的种类无穷,但是却都由基本元素交互组成,只要根据一定的法则,就能得到一定的结果。
计算机技术虽然日新月异,应用软件的变化也无止无尽,而其基本因子却非常有限。各种微处理器的汇编语言,正是计算机软件的基础结构,任何要通过软件以完成的动作,都是经由汇编语言的指令群,逐步执行的。
因为计算机结构复杂,各种任务分工极精,即使是一位资深的高级程序员,终其生也不过局限在若干固定的程序中钻研,很难以宏观的立场认知全貌。再加上市场压力,局外人莫名其奥妙,局中人又忙得不可开交,所以还没有任何人能作出全盘的评估。
汇编语言首先成为被误解的牺牲者,包括应用它的系统工程师在内,都一致认为它「难学难用」,(中文也是一种组合形式的应用,其所组合者是人的概念。无独有偶,人们在不求甚解之余,都视之为畏途。)事实上大谬不然,现在是科学挂帅,而科学的精义就在于系统的分类和应用。问题是我们能不能归纳出一些学习、应用的法则,将组合的过程化繁为简,以符合各种应用范畴。
二、个人体验
我个人对此感受极为深切,我原是个十足的外行,1978年第一次接触计算机,曾以不到两周的时间,就学会计算机操作,并应用「培基语言」设计完成"仓颉输入"程序。当时我认为培基语言易学易用,是计算机上最好的工具。
后来,我开始用培基语言设计"仓颉向量组字"程序,每秒可生成两个字,当时与我合作的宏碁公司建议我采用汇编语言,他们说组字程序速度要快,培基语言不能胜任。如改用汇编语言,效率可提高十倍,由此开始了我与汇编语言的不解之缘。1979年9月我们正式推出了由国人自行设计、具有完整的计算机功能、可运用数万中文字的"天龙中文计算机"。
宏碁公司动用了三位资深工程师,采用 Z80 MCZ系统,以六个月的时间完成了向量组字及系统程序,记忆空间占60KB,处理速度每秒约组成30字。
这是我首次发现到汇编语言的威力,深究之下,才理解到计算机的全部工作原理。简单说来,汇编语言就是组合计算机所有功能的控制指令,利用它,就可以直接控制计算机。
其它高级语言,只是让人省事,用一些格式化的手续,把人的想法化为过程的指令,这种情形就相当于为了迁就开车的人,建了密如蛛网的高速公路。本来走路只要几分钟就可到达的地方,以车代步的结果,反而需要耗费半个小时。
1980年,我决定自己动手,又重新设计了一套字数较多,字形较美观的组字程序。只用了三个月的时间,结果不仅记忆空间缩小了三分之一,速度也快了十倍,达到每秒 300字。这个产品,就是1苹果机上用的「汉卡」。
1983年,再经分析,我发现以往写的程序很不精简,技术也不成熟。我坚信中文字形在计算机上的应用,将是中国文化存亡兴衰的根本因素,不仅值得投注自己的时间及精力,且也有此必要。所以我又拋掉了一切,重头设计,加入更多的变化参数,并根据人的辨识原理,设计成第三代至第五代等多种字形产生器。每一代之间,速度都明显地提高,功能也不断加强。在这样一再重复的摸索中,尝试了各种可行的途径,充份认识了汇编语言的特性及长处。
由于汇编语言灵活无比的特性,我发现它就如同画家的画笔一般,只为了牟利,可以用它画成各种廉价速成的商品;一旦投入自己的理想与心智,画笔就不再只是一枝笔,而成为人心与外界的界面,画出的作品立时升华成为艺术,进入一个更高的境界!
1985年,我再次重新设计规划,采用人的智能原则,把人写字、认字的观念化为数据结构,程序只是用来阐释数据、控制计算机的界面。该字库的字形可做到无级次放大缩小,字体、字型皆能任意变化 (每字可以产生数亿种变形) 。而且除了现今各种字典已收的六万余字外,还可以组成完全符合中文规则的新字六百万个,足敷未来新时代新观念的发挥应用。
不仅如此,组字速度又提高了,每秒可以组成 30*30的字形两千个!当然现在用的是15MHZ 80286 ,比以往的4.75 MHZ的Z80 已经快了近六倍。但是,改良后的新程序,其功能的增加,处理过程的繁杂性已远非当年可比。
这些成果,用了很多特殊的数据结构技巧,不可能经由高级语言来完成。既然用汇编语言所制作的程序能一再大幅度地改进,这就说明了汇编语言的弹性极大,效率相去千里。如不痛下苦功钻研,程序写完,能执行就算了事,又怎能领悟其中奥妙?
所以,我并不认为汇编语言只是一种程序语言而已,它是一种创造艺术品的工具,它能赋与无知无觉的电子机器一种「生命」,由无知进而有知,由有知而生智能。通过对汇编语言的研究探索,我整理出一些规律,写成这本书,以便于理解及应用。但是,要真正将汇编语言发展成为艺术,尚有待青年朋友们继续努力,在这个信息时代,开拓出一片崭新的天地。
无意义的音符能编成美妙的音乐,无规律的色彩可幻化为缤纷的世界,为什么计算机的机器指令,不能架构出信息的理性天地?
这就是艺术,作为艺术家,就必须奉献出自己的心血,以真、善、美为最高境界。
要达到这种目的,就要认真的作好准备动作,再一步一步地追求下去。
三、利人与利己
任何一种商业产品,当然是以利益为先,利己后而利人。如果是艺术品创造,则刚刚相反,唯有能忽视己利,沥血泣心地探索,虔诚狂热地奉献,才会迸发出人性的光辉,创造不朽的杰作。
艺术家之伟大,在于此,人性之可贵,在于此。
对组合程序语言,有人视为商品,将写作技巧当作专利,轻不示人。相信这也是迄今尚无一本象样的参考书籍之根本原因,我买了不少这类书,但书中除了指令介绍以及编程、侦错的手续外,完全没有技巧的说明,好象懂得指令就可以把程序写好一般。当我自己下了不少功夫,得到了一些心得,再回过头来看那些参考书,才发现连作者本人所举的例子,都是平铺直叙,毫无技巧可言。
(更正,在序言中我曾提到有本最近出版的"禅-汇编语言",是唯一的例外,希望读者不要错过。)
多年来,我一直想写本有关汇编语言写作技巧的书,可惜都得不到机会。这次,为了实现「整合系统」革命性的计划,所有招收的工程师,一概从头训练。由于没有可用的教材,只好自己动手,于是初步有了讲义,再经修改,便成此书。
我认为,既然汇编语言是种艺术,我们不仅不应该藏私自珍,而且要相互探讨,交流切磋,以期发扬光大。
不过,技术本身与利用该技术所创造的产品却不能混为一谈,产品是借以谋生的工具,能够生存,大家才有研究发展的机会,也才能把成果贡献给社会。如果国人不尊重别人的产品权利,只是互相抄袭盗用,或能受惠于一时,但影响所及,人人贪图现成,不事发展,则观念停顿,技术落伍,其后果不堪设想。
\newpage
\section{前言}
\subsection{关于本书}
本书主要介绍了Vulkan的图形和计算API。Vulkan是Khronos组织(该组织以OpenGL闻名)发布的新一代图形API。这一新的API可以更好地描述应用程序所需的运算,并且相比于OpenGL和Direct3D,拥有更好的性能,更轻便的驱动程序。Vulkan在设计上类似于Direct3D 12和Metal,但比之后两者,Vulkan是跨平台的,可以在Windows,Linux和Android平台上使用。
使用Vulkan不是没有缺点,更精准的控制,意味着更繁琐的细节。我们需要在应用程序中做更多的工作,这包括设置初始时使用的帧缓冲,以及对缓冲和纹理图像的内存管理。
Vulkan并非适合所有人。它是为追求计算机图形性能极限的狂热分子设计的。如果你对游戏开发更感兴趣,或许OpenGL和Direct3D更适合你,它们在短期内仍然会是主流,并且目前还有大量的设备尚未支持Vulkan。除此之外,也可以使用Unreal Engine或Unity这类引擎,它们可以通过封装好的底层完全透明地使用Vulkan。
下面是学习本书的一些先决条件:
\begin{itemize}
\item 支持Vulkan的显卡以及驱动程序(NVIDIA,AMD,Intel)
\item C++编程经验(熟悉RAII,初始化列表)
\item 支持C++11的编译器(Visual Studio 2013+,GCC 4.8+)
\item 三维计算机图形学基础
\end{itemize}
本书不假设读者了解OpenGL和Direct3D,但需要读者了解基本的三维计算机图形学知识。
读者可以使用C来替换C++,但这样做需要读者对我们的代码进行大量修改。我们的代码使用了类,RAII等C++特性。
\subsection{电子书}
本书有两种格式的电子版提供:
\begin{itemize}
\item EPUB
\item PDF
\end{itemize}
\subsection{教程结构}
我们首先简要介绍Vulkan的工作原理,然后介绍如何使用Vulkan在屏幕上绘制一个三角形。接着,我们介绍如何配置开发环境来使用Vulkan SDK,在这里,我们还引入了GLM库来进行线性代数运算,引入了GLFW库来进行窗口的创建。教程包含了在Windows上使用Visual Studio的配置方法,在Ubuntu上使用GCC的配置方法。
之后,我们将会实现Vulkan绘制三角形的必要部分。每一章节大致遵循下面的结构:
\begin{itemize}
\item 介绍新概念,以及它的用途
\item 将与之相关的API调用集成到我们的程序中去
\item 封装辅助函数
\end{itemize}
尽管章节的组织有一定顺序,但对于部分章节,完全可以独立阅读,作为一个Vulkan特性的介绍。也就是说,除了作为教程外,本书可以作为Vulkan的一个参考手册,当作字典来查询。书中所有Vulkan函数和类型都被超链接到了Vulkan规范,可以通过鼠标点击,获取它们更加详细的信息。由于Vulkan是一个非常新的API,它的规范文档可能存在许多不足,读者可以提交反馈给Khronos的Github仓库。
之前提到,为了更精准地控制硬件,使用Vulkan需要处理大量细节。这造成许多很基础的操作也需要编写很多代码才能完成。为了简化这类操作的处理,我们会编写一些辅助函数。
每一章节也都包含了总结,以及到此为止的完整代码的超链接。如果读者存在疑惑的地方,可以参考这些代码。这些代码经过了多个不同厂商的显卡测试。
Vulkan是一个非常新的图形API,有关它的最佳实践尚未建立。如果你对本书有任何建议,可以提交反馈到Github仓库。
完成使用Vulkan绘制三角形的程序后,我们将对其进行扩展,引入线性变换,纹理和三维模型。
如果读者在之前使用过图形API,应该知道在几何体显示到屏幕之前,需要经过很多步骤。使用Vulkan同样是这样,但这些步骤很容易理解,并且每一步都不多余。绘制三维模型采取的步骤并不比绘制三角形所采取的步骤多很多。
如果在实践本书的过程中遇到问题,读者可以先查看本书的FAQ,是否存在关于这一问题的解决方案,没有的话,欢迎在相关章节进行评论来寻求帮助。
准备好体验高性能次世代图形API了吗?让我们开始吧!
\newpage
\section{概述}
本章节首先简要介绍了Vulkan,以及它所解决的问题。然后,我们将会看到如何使用Vulkan来绘制一个三角形,建立Vulkan的基本使用思路。最后,我们将会介绍Vulkan API的基本结构和使用方式。
\subsection{Vulkan起源}
Vulkan是作为一个跨平台的图形API设计的。以往许多图形API采用固定功能渲染管线设计,应用程序按照一定格式提交顶点数据,配置光照和着色选项。
随着显卡架构逐渐成熟,提供了越来越多的可编程功能,这些功能被集成到原有的API中。造成驱动程序要做的工作越来越复杂,应用程序开发者要处理的兼容性问题也越来越多。随着移动浪潮到来,人们对移动GPU的要求也越来越高,但以往的图形API不能够进行更加精准地控制来提升效率,对多线程的支持也非常不足,导致没有发挥出图形硬件真正的潜力。
由于没有历史包袱,Vulkan完全按照现代图形架构设计,提供了更加详细的API给开发者,大大减少了驱动程序的开销,允许多个线程并行创建和提交指令,使用标准化的着色器字节码,将图形和计算功能进行统一。
\subsection{画一个三角形}
现在,让我们来看下如何使用Vulkan绘制三角形。这里用到的所有概念会在下一章节进行详细地说明。
\textbf{步骤1:实例和物理设备选择}
我们的应用程序是通过VkInstance来使用Vulkan API的。应用程序创建VkInstance后,就可以查询Vulkan支持的硬件,选择其中一个或多个VkPhysicalDevices进行操作。我们可以通过查询设备属性,选择一个适合我们的设备。
\textbf{步骤2:逻辑设备和队列族}
选择完合适的硬件设备后,我们需要使用更详细的VkPhysicalDevice特性(比如多视口,64位浮点)来创建一个逻辑设备VkDevice。还需要指定我们想要使用的队列族。Vulkan将诸如绘制指令、内存操作提交到VkQueue中,进行异步执行。队列由队列族分配,每个队列族支持一个特定操作集合。比如,图形,计算和内存传输操作可以使用独立的队列族。队列族可以作为物理设备选择时的一个参考。比如,一个支持Vulkan的设备可能没有提供任何图形功能,但对于支持Vulkan的显卡设备而言,支持所有队列操作。
\textbf{步骤3:窗口表面和交换链}
如果不是进行离屏渲染,通常我们需要创建一个窗口来显示渲染的图像。这一工作可以通过原生平台的窗口API或像GLFW或SDL这样的库来完成,在这里,我们使用的是GLFW,有关GLFW的更多信息,我们会在下一章介绍。
我们还需要两个组件才能完成窗口渲染:窗口表面(VkSurfaceKHR)和交换链(VkSwapChainKHR)。可以注意到这两个组件都有一个KHR后缀,这表示它们属于Vulkan扩展。Vulkan API本身是完全平台无关的,需要我们使用WSI(Window System Interface,窗口系统接口)扩展与原生的窗口管理器进行交互。表面(Surface)是一个跨平台抽象,通常它是由原生窗口系统句柄作为参数实例化得到。不过,这一部分工作,GLFW已经帮我们处理了,所以不用我们关心。
交换链是一个渲染目标集合。它可以保证我们正在渲染的图像和当前屏幕图像是两个不同的图像。这可以确保显示出来的图像是完整的。每次绘制一帧时,可以请求交换链提供一张图像。绘制完成后,图像被返回到交换链中,在之后某个时刻,图像被显示到屏幕上。渲染目标数量和图像显示到屏幕的时机依赖于显示模式。常用的显示模式有双缓冲(vsync,垂直同步)和三缓冲。我们将在创建交换链章节讨论这些问题。
\textbf{步骤4:图像视图和帧缓冲}
从交换链获取图像后,还不能直接在图像上进行绘制,需要将图像先包装进VkImageView和VkFramebuffer中去。一个图像视图可以引用图像的特定部分,一个帧缓冲可以引用图像视图作为颜色,深度和模板目标。交换链中可能有多个不同的图像,我们可以预先为它们每一个都创建好图像视图和帧缓冲,然后在绘制时选择对应的那个。
\textbf{步骤5:渲染流程}
渲染流程描述了渲染操作使用的图像类型,图像的使用方式,图像的内容如何处理。对于我们这个绘制三角形的程序,我们使用了一张图像作为颜色目标,在执行绘制操作前清除整个图像。渲染流程只描述了图像的类型,图像绑定是通过VkFramebuffer完成的。
\textbf{步骤6:图形管线}
Vulkan的图形管线可以通过VkPipeline对象建立。它描述了显卡的可配置状态,比如视口大小和深度缓冲操作,以及使用VkShaderModule对象的可编程状态。VkShaderModule对象由着色器字节码创建而来。驱动程序知道哪些渲染目标被图形管线使用。
Vulkan与之前的图形API的一个最大不同是几乎所有图形管线的配置都需要提前完成。这意味着如果我们想要使用另外一个着色器或者顶点布局,需要重新创建整个图形管线。显然效率很低,这迫使我们提前创建出所有我们需要的图形管线,在需要时直接使用已经创建好的图形管线。图形管线只有很少一部分配置可以动态修改,比如视口大小和清除颜色。图形管线的所有状态也需要显式地描述,比如,不存在默认的颜色混合状态。
这样做的好处类似于预编译相比于即时编译,驱动程序可以有更大的优化空间,并且以图形管线为切换单位,渲染效果的预期也变得十分容易,不用担心切换时,遗漏某个微小的设置,造成结果的巨大差异。
\textbf{步骤7:指令池和指令缓冲}
之前提到,Vulkan的许多操作需要提交到队列才能执行。这些操作首先被记录到一个VkCommandBuffer对象中,然后提交给队列。VkCommandBuffer对象由一个关联了特定队列族的VkCommandPool分配而来。为了绘制三角形,我们需要记录下列操作到VkCommandBuffer对象中去:
\begin{itemize}
\item 开始渲染
\item 绑定图形管线
\item 绘制三角形
\item 结束渲染
\end{itemize}
由于帧缓冲绑定的图像依赖于交换链给我们的图像,我们可以提前为每个图像建立指令缓冲,然后在绘制时,直接选择对应的指令缓冲使用。当然在每一帧记录指令缓冲也是可以的,但这样做效率很低。
\textbf{步骤8:主循环}
将绘制指令包装进指令缓冲后,主循环变得非常直白。我们首先使用vkAcquireNextImageKHR函数从交换链获取一张图像。接着使用vkQueueSubmit函数提交图像对应的指令缓冲。最后,使用vkQueuePresentKHR函数将图像返回给交换链,显示图像到屏幕。
提交给队列的操作会被异步执行。我们需要采取同步措施比如信号量来确保操作按正确的顺序执行。绘制指令的执行必须在获取图像之后,否则,可能会出现读写冲突,屏幕正在读取图像数据的同时,绘制操作在进行绘制操作,造成屏幕读取显示的数据并非来自同一帧。同样,vkQueuePresentKHR函数调用需要在绘制完成后进行。
\subsection{总结}
本章节通过绘制一个简单的三角形来使读者建立Vulkan的基本使用思路。通常,一个真正实用的程序会包含更多的步骤,比如分配顶点缓冲,创建Uniform缓冲,上传纹理图像等等。但为了降低学习难度,我们从最简单的形式开始,逐步复杂化。
对于绘制一个三角形,我们需要采取的步骤包括:
\begin{itemize}
\item 创建一个VkInstance
\item 选择一个支持Vulkan的图形设备(VkPhysicalDevice)
\item 为绘制和显示操作创建VkDevice和VkQueue
\item 创建一个窗口,窗口表面和交换链
\item 将交换链图像包装进VkImageView
\item 创建一个渲染层指定渲染目标和使用方式
\item 为渲染层创建帧缓冲
\item 配置图形管线
\item 为每一个交换链图像分配指令缓冲
\item 从交换链获取图像进行绘制操作,提交图像对应的指令缓冲,返回图像到交换链
\end{itemize}
看起来步骤非常多,但其实每一步都非常简单。在接下来的章节,我们会对每一步进行非常详细地说明。如果你对程序中的某一步感到困惑,可以回来参考一下本章节。
\subsection{API概念}
本小节对Vulkan API的结构进行简要的介绍。
\subsubsection{编码约定}
Vulkan的所有函数、枚举和结构体都被定义在vulkan.h中,我们可以在Vulkan SDK中找到这一头文件。在下一章节,我们会介绍如何安装Vulkan SDK。
Vulkan API的函数都带有一个小写的vk前缀,枚举和结构体名带有一个Vk前缀,枚举值带有一个VK\_前缀。Vulkan对结构体非常依赖,大量函数的参数由结构体提供。比如,Vulkan创建对象的一般形式如下:
\begin{lstlisting}[language={[ANSI]C},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines = true]
VkXXXCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_XXX_CREATE_INFO;
createInfo.pNext = nullptr;
createInfo.foo = ...;
createInfo.bar = ...;
VkXXX object;
if (vkCreateXXX(&createInfo, nullptr, &object) != VK_SUCCESS) {
std::cerr << "failed to create object" << std::endl;
return false;
}
\end{lstlisting}
Vulkan的许多结构体需要我们通过设置sType成员变量来显式指定结构体类型。结构体的pNext成员可以指向一个扩展的结构体,在本教程,我们不使用它,它被设置为nullptr。Vulkan中创建和销毁对象的函数都有一个VkAllocationCallbacks参数,可以被用来自定义内存分配器,在这里,我们不使用它,将其设置为nullptr。
几乎所有Vulkan都会返回一个VkResult来表示调用的执行情况,它的值要么是VK\_SUCCESS,要么是一个错误代码。Vulkan规范文档描述了这些函数返回的错误代码的意义。
\subsection{校验层}
之前提到,Vulkan的设计目标是高性能、低驱动程序开销。所以,默认情况下,它提供的错误检测和调试功能非常有限。驱动程序会在发生错误时直接崩溃,而不是返回一个错误代码。这可能导致对于某种显卡可以工作,不会崩溃,但对于其它显卡无法工作,驱动程序崩溃。
可以通过Vulkan的校验层(Validation layers)特性来进行一定的错误检查措施。校验层是一段被插入在Vulkan API和驱动程序之间的代码,可以对Vulkan API函数的参数进行检查,跟踪内存分配。我们可以在开发期开启校验层,然后在发布程序时关闭校验层,减少性能损失。校验层可以完全自己编写,但为了方便,我们的教程直接使用了Vulkan SDK提供的一组校验层。我们通过注册的回调函数来接受来自校验层的调试信息。
由于Vulkan的每个操作都要显式定义,加之校验层的使用,调试使用Vulkan的程序要比调试使用OpenGL和Direct3D的程序轻松太多。
接下来让我们配置开发环境,开始我们的Vulkan编程之旅吧!
\newpage
\section{开发环境}
在本章节,我们将介绍如何配置Vulkan SDK和一些非常有用的库。所有在这里用到的工具除了编译器外,适用于Windows,Linux和MacOS三个平台,但它们的安装方法可能在不同平台会有所不同,所以在这里我们按平台分别描述如何配置它们。
\subsection{Windows}
我们这里使用Visual Studio 2017作为Windows平台的开发环境,当然使用Visual Studio 2013或2015应该也不会有任何问题,只是配置方法可能会略微不同。
\subsubsection{Vulkan SDK}
Vulkan SDK是使用Vulkan开发应用程序必不可少的组件。它包含了Vulkan API的头文件,一个校验层实现,调试工具和Vulkan函数加载器。Vulkan函数加载器类似OpenGL的GLEW可以在运行时查询驱动程序支持的Vulkan API函数。
Vulkan SDK可以从LunarG的网站上免费下载。
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-1.jpg}
\end{figure}
安装Vulkan SDK后,我们需要验证下我们的显卡和驱动程序是否支持Vulkan。这可以通过运行Vulkan SDK自带的cube.exe来完成,我们可以在Vulkan SDK安装目录下的Bin目录下找到它,运行后,可以看下到下面的窗口:
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-2.jpg}
\end{figure}
如果没有看到这个窗口,而是出现了一条错误消息,可以尝试更新显卡的驱动程序到最新版本,再次尝试,如果仍然出现错误消息,可以在显卡官网查询自己的显卡是否支持Vukan。
在Bin目录下还有一个非常有用的程序:glslangValidator.exe。它可以将GLSL代码编译为字节码。我们会在着色器模块章节,对它进行更为详细地说明。除此之外,Bin目录下还包含了Vulkan函数加载器和校验层的二进制文件,它们的库文件则位于Vulkan SDK的Lib目录下。
Vulkan SDK的Documentation目录包含了Vulkan SDK的离线文档和完整的Vulkan规范文档。最后是Vulkan SDK的Include目录,它包含了Vulkan API的头文件。除此之外,还有很多文件和目录,但对于我们的教程来说,并没有直接用到它们,所以就不再一一介绍。
\subsubsection{GLFW}
之前提到,Vulkan是一个平台无关的图形API,它没有包含任何用于创建窗口的功能。为了跨平台和避免陷入Win32的窗口细节中去,我们使用GLFW库来完成窗口相关操作,GLFW库支持Windows,Linux和MacOS。当然,还有其它一些库可以完成类似功能,比如SDL。但除了窗口相关处理,GLFW库对于Vulkan的使用还有其它一些优点。
读者可以再GLFW的官方网站上免费下载到最新版本的GLFW库。在本教程,我们使用64位版本的GLFW库,但32位版本也是可以的,只是编译使用Vulkan的应用程序时也需要链接到32位版本的Vulkan API,也就是链接到Vulkan SDK下Lib32目录下的库。下载GLFW后,将它解压缩到一个合适的位置。这里,我们将它解压到Visual Studio目录下的Libraries目录中。不要纠结于为什么解压后不存在libvc-2017目录,我们的Visual Studio 2017是完全兼容lib-vc2015的。
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-3.png}
\end{figure}
\subsubsection{GLM}
和DirectX 12不同,Vulkan没有包含线性代数库,我们需要自己找一个。GLM就是一个我们需要的线性代数库,它经常和OpenGL一块使用。
GLM是一个只有头文件的库,我们只需要下载它的最新版,然后将它放在一个合适的位置,就可以通过包含头文件的方式使用它。
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-4.jpg}
\end{figure}
\subsubsection{配置Visual Studio}
现在,我们可以创建一个基本Visual Studio工程来验证我们安装的依赖是否可以正常工作。
首先,启动Visual Studio,然后选择Windows Destop Wizard。
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-5.jpg}
\end{figure}
我们选择使用Console Application (.exe)应用程序类型,这样做我们就可以直接将调试信息输出到控制台窗口上。另外,我们将Empty Project选项打勾来阻止Visual Studo添加模板代码。
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-6.jpg}
\end{figure}
创建项目后,我们添加一个C++源代码文件到项目中。
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-7.jpg}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-8.jpg}
\end{figure}
下面的代码是这个C++源文件的内容。源代码的内容暂时不需要理解,我们现在只是为了验证我们的依赖是否配置正确,源代码的内容,我们会在后面的章节详细说明。
\begin{lstlisting}[language={[ANSI]C},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/vec4.hpp>
#include <glm/mat4x4.hpp>
#include <iostream>
int main() {
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
GLFWwindow* window = glfwCreateWindow(800, 600, "Vulkan window", nullptr, nullptr);
uint32_t extensionCount = 0;
vkEnumerateInstanceExtensionProperties(nullptr,
&extensionCount, nullptr);
std::cout << extensionCount << " extensions supported" << std::endl;
glm::mat4 matrix;
glm::vec4 vec;
auto test = matrix * vec;
while(!glfwWindowShouldClose(window)) {
glfwPollEvents();
}
glfwDestroyWindow(window);
glfwTerminate();
return 0;
}
\end{lstlisting}
现在,让我们开始配置项目属性,选择All Configurations,让设置对Debug和Release模式都有效。
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-9.jpg}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-10.jpg}
\end{figure}
打开C++ -> General -> Additional Include Directories,点击Additional Include Directories的<Edit...>下拉选项。
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-11.jpg}
\end{figure}
添加Vulkan,GLFW和GLM的头文件目录:
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-12.jpg}
\end{figure}
接着,打开Linker -> General:
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-13.jpg}
\end{figure}
添加Vulkan和GLFW的库目录:
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-14.jpg}
\end{figure}
打开 Linker -> Input,点击Additional Dependencies的<Edit...>下拉选项:
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-15.jpg}
\end{figure}
添加Vulkan和GLFW的库文件:
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-16.jpg}
\end{figure}
现在可以关闭项目属性对话框了。如果一切顺利,我们的代码编辑器里已经没有任何高亮出的错误代码了。
最后,确认我们的代码在64位模式下编译:
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-17.jpg}
\end{figure}
然后,按下F5编译运行,你就会看到下面的窗口:
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-18.jpg}
\end{figure}
控制台窗口显示的扩展数应该是非0的。至此,我们就配置好了Vulkan的开发环境!
\subsection{Linux}
对于Linux平台的配置,我们使用Ubuntu作为演示,其它Linux平台的配置方法应该是类似的。我们使用二进制包来安装Vulkan SDK,使用GCC 4.8以上版本作为编译器,使用CMake和make作为构建系统。
\subsubsection{Vulkan SDK}
Vulkan SDK是使用Vulkan开发应用程序必不可少的组件。它包含了Vulkan API的头文件,一个校验层实现,调试工具和Vulkan函数加载器。Vulkan函数加载器类似OpenGL的GLEW可以在运行时查询驱动程序支持的Vulkan API函数。
Vulkan SDK可以从LunarG的网站上免费下载。
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-19.jpg}
\end{figure}
打开终端,调整当前目录到我们下载的Vulkan SDK安装文件所在目录,然后使用下面的代码运行它:
\begin{lstlisting}[language={bash},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
chmod +x vulkansdk-linux-x86_64-xxx.run
./vulkansdk-linux-x86_64-xxx.run
\end{lstlisting}
安装文件运行后会将Vulkan SDK的所有文件导出到当前目录的VulkanSDK文件夹中。我们可以自己将VulkanSDK文件夹移动到合适的位置。
Vulkan SDK的根目录有一个build\_examples.sh脚本,执行它构建Vulkan SDK的示例程序需要我们安装XCB库,以及一些X窗口的开发文件,可以通过在终端运行下面的代码来安装这些所需的库:
\begin{lstlisting}[language={bash},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
sudo apt install libxcb1-dev xorg-dev
\end{lstlisting}
然后,我们就可以执行build\_examples.sh了:
\begin{lstlisting}[language={bash},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
./build_examples.sh
\end{lstlisting}
如果编译成功,在./examples/build/下就会出现一个cube可执行文件,运行它,可以看到下面的画面:
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-20.jpg}
\end{figure}
如果没有看到,而是出现了一条错误消息,可以尝试更新显卡的驱动程序到最新版本,再次尝试,如果仍然出现错误消息,可以在显卡官网查询自己的显卡是否支持Vukan。
\subsubsection{GLFW}
之前提到,Vulkan是一个平台无关的图形API,它没有包含任何用于创建窗口的功能。为了跨平台和避免陷入X11的窗口细节中去,我们使用GLFW库来完成窗口相关操作,GLFW库支持Windows,Linux和MacOS。当然,还有其它一些库可以完成类似功能,比如SDL。但除了窗口相关处理,GLFW库对于Vulkan的使用还有其它一些优点。
这里,由于Vulkan需要较新版本的GLFW才能支持。所以,我们使用源代码来编译安装GLFW。读者可以从GLFW的官方网站免费下载到GLFW的最新源码包。下载完成后,我们将源码包解压,使用终端进入解压的源码所在的文件夹,执行下面的代码生成makefile文件,然后编译GLFW:
\begin{lstlisting}[language={bash},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
cmake .
make
\end{lstlisting}
可能会出现Could NOT find Vulkan的警告信息,可以放心地忽略掉它。编译完成后,使用下面的代码将GLFW安装到系统的库目录中:
\begin{lstlisting}[language={bash},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
sudo make install
\end{lstlisting}
\subsubsection{GLM}
和DirectX 12不同,Vulkan没有包含线性代数库,我们需要自己找一个。GLM就是一个我们需要的线性代数库,它经常和OpenGL一块使用。
GLM是一个只有头文件的库,我们只需要下载它的最新版,然后将它放在一个合适的位置,就可以通过包含头文件的方式使用它。
这里我们直接在终端使用下面的代码安装它:
\begin{lstlisting}[language={bash},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
sudo apt install libglm-dev
\end{lstlisting}
\subsubsection{配置makefile文件}
现在,我们已经安装完了所有的依赖项,可以开始配置应用程序的makefile,验证安装是否正确。
在一个合适的位置新建一个叫做VulkanTest的文件夹,然后在文件夹里创建包含下面代码的main.cpp源代码文件。
\begin{lstlisting}[language={[ANSI]C},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/vec4.hpp>
#include <glm/mat4x4.hpp>
#include <iostream>
int main() {
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
GLFWwindow* window = glfwCreateWindow(800, 600, "Vulkan
window", nullptr, nullptr);
uint32_t extensionCount = 0;
vkEnumerateInstanceExtensionProperties(nullptr,
&extensionCount, nullptr);
std::cout << extensionCount << " extensions supported" << std::endl;
glm::mat4 matrix;
glm::vec4 vec;
auto test = matrix * vec;
while(!glfwWindowShouldClose(window)) {
glfwPollEvents();
}
glfwDestroyWindow(window);
glfwTerminate();
return 0;
}
\end{lstlisting}
源代码的内容暂时不需要理解,我们现在只是为了验证我们的依赖是否配置正确,源代码的内容,我们会在后面的章节详细说明。
接着,我们需要编写makefile来编译源代码。这里假设读者具有一定makefile使用经验,知道makefile的变量和规则的用法。如果没有,也可以从本教程中快速学习这些知识。
我们首先定义一些变量来简化makefile编写。VULKAN\_SDKPATH变量存放了Vulkan SDK的x86\_64目录的位置:
\begin{lstlisting}[language={bash},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
VULKAN_SDK_PATH = /home/user/VulkanSDK/x.x.x.x/x86_64
\end{lstlisting}
读者应该替换上面代码的路径为自己Vulkan SDK的实际路径。接着,我们定义CFLAGS变量来指定编译选项:
\begin{lstlisting}[language={bash},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
CFLAGS = -std=c++11 -I$(VULKAN_SDK_PATH)/include
\end{lstlisting}
上面的代码表示使用C++ 11来编译源代码,将Vulkan SDK的包含目录加入编译器的包含目录搜索路径中。
然后,定义LDFLAGS变量来指定链接选项:
\begin{lstlisting}[language={bash},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
LDFLAGS = -L$(VULKAN_SDK_PATH)/lib `pkg-config --static --libs glfw3` -lvulkan
\end{lstlisting}
上面的代码将Vulkan SDK的库路径加入链接器的库搜索路径中,链接了Vulkan SDK的vulkan库,使用pkg-config命令取得了glfw静态链接选项。
现在可以开始定义编译VulkanTest的规则了:
\begin{lstlisting}[language={bash},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
VulkanTest: main.cpp
g++ $(CFLAGS) -o VulkanTest main.cpp $(LDFLAGS)
\end{lstlisting}
验证规则是否正确,可以将上面的代码保存为Makefile文件,然后使用终端在Makefile文件所在目录执行make命令。如果一切顺利,会生成一个VulkanTest可执行文件。
现在,我们定义另外两个规则,test和clean,前一个规则用于执行生成的可执行文件,后一个规则用于清除生成的可执行文件:
\begin{lstlisting}[language={bash},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
.PHONY: test clean
test: VulkanTest
./VulkanTest
clean:
rm -f VulkanTest
\end{lstlisting}
验证规则能否执行后,读者可能会发现make clean工作的非常好,但make test却产生了下面的错误信息:
\begin{lstlisting}[language={bash},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
./VulkanTest: error while loading shared libraries:
libvulkan.so.1: cannot open shared object file: No such file or directory
\end{lstlisting}
这是因为libvulkan.so没有被安装在系统的库目录,无法被VulkanTest加载。我们可以通过LD\_LIBRARY\_PATH环境变量显式指定库目录来解决这个问题:
\begin{lstlisting}[language={bash},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
test: VulkanTest
LD_LIBRARY_PATH=$(VULKAN_SDK_PATH)/lib ./VulkanTest
\end{lstlisting}
现在make test应该可以成功执行VulkanTest了。
\begin{lstlisting}[language={bash},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
test: VulkanTest
LD_LIBRARY_PATH=$(VULKAN_SDK_PATH)/lib\
VK_LAYER_PATH=$(VULKAN_SDK_PATH)/etc/explicit_layer.d\
./VulkanTest
\end{lstlisting}
至此,我们的Makefile文件已经编写完毕了,它的所有内容如下:
\begin{lstlisting}[language={bash},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
VULKAN_SDK_PATH = /home/user/VulkanSDK/x.x.x.x/x86_64
CFLAGS = -std=c++11 -I$(VULKAN_SDK_PATH)/include
LDFLAGS = -L$(VULKAN_SDK_PATH)/lib `pkg-config --static --libs glfw3` -lvulkan
VulkanTest: main.cpp
g++ $(CFLAGS) -o VulkanTest main.cpp $(LDFLAGS)
.PHONY: test clean
test: VulkanTest
LD_LIBRARY_PATH=$(VULKAN_SDK_PATH)/lib\
VK_LAYER_PATH=$(VULKAN_SDK_PATH)/etc/ex plicit_layer.d\
./VulkanTest
clean:
rm -f VulkanTest
\end{lstlisting}
读者可以将刚刚配置的Makefile文件作为一个模板在以后使用。
现在,让我们花点实践浏览下Vulkan SDK目录,在x86\_64/bin目录下还有一个非常有用的程序:glslangValidator。它可以将GLSL代码编译为字节码。我们会在着色器模块章节,对它进行更为详细地说明。除此之外,Bin目录下还包含了Vulkan函数加载器和校验层的二进制文件,它们的库文件则位于Vulkan SDK的Lib目录下。
Vulkan SDK的Doc目录包含了Vulkan SDK的离线文档和完整的Vulkan规范文档。最后是Vulkan SDK的Include目录,它包含了Vulkan API的头文件。除此之外,还有很多文件和目录,但对于我们的教程来说,并没有直接用到它们,所以就不再一一介绍。
至此,我们已经做好开始Vulkan探险之旅的准备!
\subsection{MacOS}
这里假定读者使用Xcode和Homebrew包管理器。另外还需要我们的MacOS版本在10.11以上,显卡设备支持Metal API。
\subsubsection{Vulkan SDK}
Vulkan SDK是使用Vulkan开发应用程序必不可少的组件。它包含了Vulkan API的头文件,一个校验层实现,调试工具和Vulkan函数加载器。Vulkan函数加载器类似OpenGL的GLEW可以在运行时查询驱动程序支持的Vulkan API函数。
Vulkan SDK可以从LunarG的网站上免费下载。
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-21.jpg}
\end{figure}
Vulkan SDK的MacOS版本是通过MoltenVK实现的,并非原生实现,也就是说MoltenVK作为一个中间层将Vulkan API调用转换为Metal调用。这也使得我们可以直接使用Metal的调试功能进行调试。
下载Vulkan SDK后,将它解压到一个合适的位置,在解压后的目录中的Applications,可以找到一些Vulkan SDK的演示程序,运行其中的可执行文件cube,你将会看到下面的窗口:
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-22.jpg}
\end{figure}
\subsubsection{GLFW}
之前提到,Vulkan是一个平台无关的图形API,它没有包含任何用于创建窗口的功能。为了跨平台和避免陷入窗口操作相关的细节中去,我们使用GLFW库来完成窗口相关操作,GLFW库支持Windows,Linux和MacOS。当然,还有其它一些库可以完成类似功能,比如SDL。但除了窗口相关处理,GLFW库对于Vulkan的使用还有其它一些优点。
我们使用Homebrew包管理器来安装GLFW库。GLFW的3.2.1稳定版目前还尚未完全支持Vulkan,所以我们使用下面的代码安装glfw3包的最新版本:
\begin{lstlisting}[language={bash},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
brew install glfw3 --HEAD
\end{lstlisting}
\subsubsection{GLM}
Vulkan没有包含线性代数库,我们需要自己找一个。GLM就是一个我们需要的线性代数库,它经常和OpenGL一块使用。
GLM是一个只有头文件的库,我们只需要下载它的最新版,然后将它放在一个合适的位置,就可以通过包含头文件的方式使用它。
这里我们直接在终端使用下面的代码安装它:
\begin{lstlisting}[language={bash},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
brew install glm
\end{lstlisting}
\subsubsection{配置Xcode}
现在所有的依赖项已经安装完毕,我们可以开始配置一个最基本的用于Xcode的Vulkan项目。
启动Xcode,然后新建一个Xcode项目,选择Application > Command Line Tool项目类型:
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-23.jpg}
\end{figure}
接着,选择C++作为项目使用的语言:
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-24.jpg}
\end{figure}
现在将下面的代码作为项目的main.cpp源文件的内容:
\begin{lstlisting}[language={[ANSI]C},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/vec4.hpp>
#include <glm/mat4x4.hpp>
#include <iostream>
int main() {
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
GLFWwindow* window = glfwCreateWindow(800, 600, "Vulkan
window", nullptr, nullptr);
uint32_t extensionCount = 0;
vkEnumerateInstanceExtensionProperties(nullptr,
&extensionCount, nullptr);
std::cout << extensionCount << " extensions supported" << std::endl;
glm::mat4 matrix;
glm::vec4 vec;
auto test = matrix * vec;
while(!glfwWindowShouldClose(window)) {
glfwPollEvents();
}
glfwDestroyWindow(window);
glfwTerminate();
return 0;
}
\end{lstlisting}
源代码的内容暂时不需要理解,我们现在只是为了验证我们的依赖是否配置正确,源代码的内容,我们会在后面的章节详细说明。
现在Xcode应该会显示一些诸如库未找到的错误。我们接下来的工作就是解决这些错误。在Project Navigator面板选择我们的项目,然后打开Build Settings标签页,进行下面的操作:
\begin{itemize}
\item 将/usr/local/include加入Header Search Paths,这是Homebrew安装头文件的路径,我们安装的glm和glfw3的头文件都在该文件夹下,然后将vulkansdk/macOS/include加入Header Search Paths,vulkansdk为我们安装的Vulkan SDK的目录。这样Xcode就可以找到我们使用的库的头文件。
\item 将/usr/local/lib加入Library Search Paths,这是Homebrew安装库文件的路径,我们安装的glm和glfw3的库文件都在该文件夹下,然后将vulkansdk/macOS/lib加入Library Search Paths,vulkansdk为我们安装的Vulkan SDK的目录。这样Xcode就可以找到我们使用的库文件。
\end{itemize}
设置完成后,看起来像这样(实际内容依赖于我们自己的文件所在的位置):
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-25.jpg}
\end{figure}
现在,点击 Build Phases标签页,添加glfw3和vulkan框架。这里,为了简便,我们添加的是动态库(如果想要使用静态库,可以参考这些库的官方文档)。
\begin{itemize}
\item 对于glfw,打开/usr/local/lib目录,可以找到类似libglfw.3.x.dylib形式的文件(x是库的版本号,依赖于我们使用Homebrew下载安装的glfw的版本)。将这个文件拖拽到Linked Frameworks and Libraries标签页即可。
\item 对于Vulkan,打开vulkansdk/macOS/lib目录(vulkansdk是我们的Vulkan SDK所在目录),拖拽libvulkan.1.dylib和libvulkan.1.x.xx.dylib文件到Linked Frameworks and Libraries标签页即可。
\end{itemize}
完成上面的操作后,更改Copy Files标签页下的Destination为Frameworks,然后清空Subpath文本框,去掉勾选Copy only when installing,点击+号,将所有三个动态库添加进去。
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-26.jpg}
\end{figure}
最后,我们需要配置环境变量。在Xcode的工具栏上通过Product > Scheme > Edit Scheme...打开Arguments标签页,添加下面的环境变量:
\begin{itemize}
\item VK\_ICD\_FILENAMES = vulkansdk/macOS/etc/vulkan/icd.d/MoltenVK\_icd.json
\item VK\_LAYER\_PATH = vulkansdk/macOS/etc/vulkan/explicit\_layer.d
\end{itemize}
完成后,看起来像这样:
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-27.jpg}
\end{figure}
至此为止,我们完成了全部设置,可以编译运行项目了,效果如下:
\begin{figure}[H]
\centering
\includegraphics[scale=0.5]{img/f3-28.jpg}
\end{figure}
程序输出的日志信息中的扩展数应该是非0的
。
现在,我们已经做好开始Vulkan探险之旅的准备!
\newpage
\section{基础代码}
\subsection{一般结构}
在本章节,我们开始使用Vulkan API编写代码。
\begin{lstlisting}[language={[ANSI]C},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
#include <vulkan/vulkan.h>
#include <iostream>
#include <stdexcept>
#include <functional>
#include <cstdlib>
class HelloTriangleApplication {
public:
void run() {
initVulkan();
mainLoop();
cleanup();
}
private:
void initVulkan() {
}
void mainLoop() {
}
void cleanup() {
}
};
int main() {
HelloTriangleApplication app;
try {
app.run();
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
\end{lstlisting}
代码中,我们首先包含了Vulkan API的头文件,它为我们提供了Vulkan API的函数,结构体和枚举。此外,包含stdexcept和iostream头文件用来报错。包含functional头文件用于资源管理。包含cstdlib头文件用来使用EXITSUCCESS和EXIT\_FAILURE宏。
我们将程序本身包装为一个类,将Vulkan对象存储为类的私有成员。我们使用initVulkan函数来初始化Vulkan对象。初始化完成后,我们进入主循环进行渲染操作。mainLoop函数包含了一个循环,直到窗口被关闭,才会跳出这个循环。mainLoop函数返回后,我们使用cleanup函数完成资源的清理。
程序在执行过程中,如果发生错误,会抛出一个带有错误描述信息的std::runtime\_error异常,我们在main函数捕获这个异常,并将异常的描述信息打印到控制台窗口。为了处理多种不同类型的异常,我们使用更加通用的std::exception来接受异常。一个比较常见的异常就是请求的扩展不被支持。
接下来的每一章,我们会添加新的成员到我们的类中,然后在initVulkan函数中初始化它们,在cleanup函数中清理它们。
\subsection{资源管理}
和使用malloc函数分配的内存块相同,使用Vulkan API创建的Vulkan对象也需要在不需要它们时显式地被清除。现代C++可以通过<memory>头文件自动地进行资源管理,但在这里,为了让大家更加清晰地理解Vulkan对象地创建和清除,以及它们的生命周期,我们没有使用它,而是手动自己完成资源管理。除此之外,Vulkan的一个核心思想就是通过显式地定义每一个操作来避免出现不一致的现象。
学完本教程后,读者可以通过重载std::shared\_ptr来实现自动资源管理。将RAII应用到自己的程序中。但对于学习而言,最好是能清晰地理解每一个细节部分。
Vulkan对象可以直接通过类似vkCreateXXX的函数,或是通过其它对象调用类似vkAllocateXXX的函数创建。当创建的对象不再使用时,使用对应的vkDestroyXXX或vkFreeXXX函数进行清除操作。这些函数的参数对于不同类型的对象通常是不同的,但都具有一个pAllocator参数。我们可以通过这个参数来指定回调函数编写自己的内存分配器。但在本教程,我们没有使用它,将它设置为nullptr。
\subsection{和GLFW交互}
Vulkan可以在完全没有窗口的情况下工作,通常,在离屏渲染时会这样做。但一般而言,还是需要一个窗口来显示渲染结果给用户。接下来,我们要完成的就是窗口相关操作。
首先替换代码中的\#include <vulkan/vulkan.h>为:
\begin{lstlisting}[language={[ANSI]C},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
\end{lstlisting}
上面的代码将GLFW库的定义包含进来,而GLFW库会自动包含Vulkan库的头文件。接着,我们添加一个叫做initWindow的函数来初始化GLFW,并在run函数中调用它:
\begin{lstlisting}[language={[ANSI]C},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
void run() {
initWindow();
initVulkan();
mainLoop();
cleanup();
}
private:
void initWindow() {
}
\end{lstlisting}
initWIndow函数首先调用了glfwInit函数来初始化GLFW库,由于GLFW库最初是为OpenGL设计的,所以我们需要显式地设置GLFW阻止它自动创建OpenGL上下文:
\begin{lstlisting}[language={[ANSI]C},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
\end{lstlisting}
窗口大小变化地处理需要注意很多地方,我们会在以后介绍它,暂时我们先禁止窗口大小改变:
\begin{lstlisting}[language={[ANSI]C},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
\end{lstlisting}
接着,我们添加了一个GLFWwindow* window变量存储我们创建的窗口句柄:
\begin{lstlisting}[language={[ANSI]C},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
window =glfwCreateWindow(800, 600, "Vulkan", nullptr, nullptr);
\end{lstlisting}
glfwCreateWindow函数的前三个参数指定了要创建的窗口的宽度,高度和标题。第四个参数用于指定在哪个显示器上打开窗口,最后一个参数与OpenGL相关,对我们没有意义。
硬编码窗口大小不是一个好习惯,所以我们定义了两个常量,以便之后可以方便地修改它们:
\begin{lstlisting}[language={[ANSI]C},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
const int WIDTH = 800;
const int HEIGHT = 600;
\end{lstlisting}
现在,我们地initWindow函数看起来应该像这样:
\begin{lstlisting}[language={[ANSI]C},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
void initWindow() {
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
}
\end{lstlisting}
为了确保我们的程序在没有发生错误和窗口没有被关闭的情况下可以一直运行,我们在mainLoop函数中添加了下面的事件循环:
\begin{lstlisting}[language={[ANSI]C},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
void mainLoop() {
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
}
}
\end{lstlisting}
上面的代码应该非常直白,每次循环,检测窗口的关闭按钮是否被按下,如果没有被按下,就执行事件处理,否则结束循环。在之后的章节,我们会在这一循环中调用渲染函数来渲染一帧画面。
一旦窗口关闭,我们就可以开始结束GLFW,然后清除我们自己创建的资源,这在cleanup函数中进行:
\begin{lstlisting}[language={[ANSI]C},keywordstyle=\color{blue!70},commentstyle=\color{red!50!green!50!blue!50},frame=shadowbox, rulesepcolor=\color{red!20!green!20!blue!20},basicstyle=\small,numbers=left, numberstyle=\tiny,breaklines=true]
void cleanup() {
glfwDestroyWindow(window);