从零开始手敲次世代游戏引擎#2

作者:转载小公举
2017-09-09
5 3 0

编者按

本文已于作者 @陈文礼 授权转载,原载于知乎,如需转载请务必联系原作者。

开始摆脱Visual Studio,建立独立的Toolchain

上一篇我们写了一个最基本的 Hello Engine,并用 Visual Studio 的命令行工具,cl.exe 进行了编译。

然而,Visual Studio 只能在Windows上面使用。而且Visual Studio对C/C++进行了很多非标准的扩展。因此基于Visual Studio写出来的程序,除非你写的时候就很清楚哪些可以用哪些不可以用,否则基本是不可以移植到别的平台的。因为Windows并不是一个POSIX (POSIX - Wikipedia)系统,也就是说是一个非常不“标准”的系统。基于这样的系统的API写出来的程序基本只能跑在这个系统上。

我打算让这个手打引擎跑在所有我可以接触到的平台上。目前我可以接触到的平台有:Windows/Linux/PS4/PSV/Android/IOS

所以我需要打造一个独立于特定平台的编译工具包,也就是Toolchain。

目前在开源领域用得比较多的Toolchain是GCC和Clang。GCC历史比较长,很多开源软件,包括Linux内核都是GCC编译的。但厚重的历史也使其很臃肿,里面包括很多已经死掉的东西。而Clang则较年轻,现在也比较流行。

另外,PS4的编译器就是基于Clang的。AMD的OpenGPU计划,以及Vulkan图形API等也是基于Clang的。苹果的最新开发平台一样是基于Clang的。所以,我选择Clang。

准备编译Clang的环境

Clang的项目页面在Clang - Getting Started

首先我们按照Clang项目页面的提示,在Windows上面安装Subversion,这个是获取Clang源代码用的。我推荐安装TortoiseSVN,这个相对比较好用。注意命令行工具缺省是不安装的,需要手工勾选安装。

Home · TortoiseSVN
1

然后是CMake。我们在Visual Studio里面建立工程的时候,会自动创建Solution和Project文件来进行代码的组织管理和编译选项的存储。然而,这些同样是只有Visual Studio才能使用的文件格式。在Linux等平台上一般是使用make,或者GNU版的make:gmake。make是依靠一个叫做Makefile的文件来存储项目文件清单和编译选项的。可以直接手写,但是文件多了一般我们就希望自动生成。况且,在不同平台上面,虽然都有C/C++编译器,能够编译C/C++代码,但是各种库的头文件、静态链接库、动态链接库的存储位置,甚至是名字都会有很微妙的差异。所以,如果直接手写Makefile,那么结果就是我们需要为每个平台单独写一个。有一些早期GNU软件就是这样的。这很不利于管理。比如我们添加了一个C++文件,那么我们就需要改所有不同版本的Makefile。

所以有一个工具叫Auto Tools,包括automake autoconf等一系列工具。这些工具可以根据一个叫做http://Makefile.am的模板(与Makefile的区别是里面基本只写项目里的文件,因为这些文件的位置是我们自己可以控制的)自动生成Makefile。这些工具可以为我们自动检测一些常见平台的差异,并在生成的Makefile里面消除这些差异。

然而这个Auto Tools本身也是足够复杂的,使用起来并不是很方便,况且不支持Windows平台。有兴趣的可以参考

Autotools Introduction

CMake是近年兴起的新秀,支持包括Windows在内的诸多平台,使用也比Auto Tools要方便不少。只需要写个CMakelists.txt就可以了。CMake在这里下载。

CMake

3

 

安装的时候,同样需要注意,因为我们工作在命令行,需要让安装程序设置环境参数,如上图。否则在命令行会找不到cmake。

接下来是Python。注意Python 2和Python 3是不兼容的。Python 2很古老但是久经考验,Python 3比较新,但是还不是很成熟。我们这里需要的是Python 2.7(因为Clang的test case是2.7接口的)。话说Python近年随着阿尔法大红大紫,因为人工智能领域用 Python 用得很多。一般来说,越是偏应用方向的(比如人工智能算法研究),越是用高阶的语言(脚本),避免在本来关心的事物之外花费时间。

Download Python

当然,我们这里安装Python是为了跑 Clang 的测试 case,确认我们自己编译出的 Clang 功能正常。这个步骤是十分重要的。因为如果是编译器的bug带来的问题,一般都可以轻易将码农坑在里面几个月出不来。比如一个变量明显代入了1,后面读出来偏偏变成了2...(CPU Cache控制问题)这种问题是最难查出来的问题之一。

最后是 GnuWin32 Tools,这是一组开源命令行工具。Linux 什么的都是自带或者可以很方便地安装的。Windows 上面就需要下载安装:

GetGnuWin32 - Maintaining a Gnuwin32 Package archive

这些工具数量众多,我们这里主要也是为了跑 Clang 的测试 Case,就不一一展开了。

需要注意的是,网页上能下载的东西只是装了个下载器,装完之后需要进入安装目标目录,执行 download.bat 和 install.bat 完成安装。之后需要更改环境变量 PATH,保证在我们的命令行里面可以找到这些工具。(具体路径请根据你安装的路径修改)

3

关于如何改Windows的环境变量,参考下面:

http://jingyan.baidu.com/article/8ebacdf02d3c2949f65cd5d0.html

好了。现在我们重新启动命令行,来使修改的环境变量生效。(命令行窗口会一直保持启动的时候的环境变量,所以改了环境变量之后需要重启命令行才能反映出我们的修改)

输入svn help,看到类似下面的输出,说明subversion安装OK了:

C:UsersTim.AzureADSource>svn help
usage: svn  [options] [args]
Subversion command-line client.
Type 'svn help ' for help on a specific subcommand.
Type 'svn --version' to see the program version and RA modules
  or 'svn --version --quiet' to see just the version number.

Most subcommands take file and/or directory arguments, recursing
on the directories.  If no arguments are supplied to such a
command, it recurses on the current directory (inclusive) by default.

Available subcommands:
   add
   ...

输入 python,看到类似下面的输出,则说明 python 安装 OK 了:

C:UsersTim.AzureAD>python
Python 2.7.13 (v2.7.13:a06454b1afa1, Dec 17 2016, 20:53:40) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>

按 Ctrl+Z,回车,退出 python 交互模式。

输入 grep,看到类似下面的输出,则说明 GnuWin32 工具也安装成功了。

C:UsersTim.AzureAD>grep
Usage: grep [OPTION]... PATTERN [FILE]...
Try `grep --help' for more information.

开始编译Clang

Clang 是基于 LLVM 的。所谓 LLVM,就是一个小小的虚拟机。这个虚拟机抽象了不同的硬件平台,如 x86/arm/mips 等。最近还抽象了 GPU。有点像 Java 的VM,但是又和 Java 的 VM 很不同。Java 的 VM 是比较高层的,它的 byte code 包括很多硬件平台并不能直接支持的功能。而 LLVM 的 byte code 则是更加接近硬件(CPU/GPU)的实际功能,只不过它是独立于任何一个具体硬件存在的。非常简单粗糙地比喻的话,各种 CPU/GPU 就好比各个地方的人,说的是各个地方的方言;而 LLVM 的 byte code 则有些像普通话,与方言有着比较类似1对1的对应关系。(当然严格地来讲并不是这么回儿事情)

所以,首先需要签出 LLVM 的代码,如下操作:

C:UsersTim.AzureADSource>svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm

Clang 是作为 LLVM 的一个前端,即,把 C/C++ 翻译为 LLVM 可以懂的 byte code 的工具。LLVM 再把 byte code 翻译为具体的机器指令。执行下面的命令签出 Clang 的代码并放在LLVM妥当的位置:

C:UsersTim.AzureADSource>cd llvmtools

C:UsersTim.AzureADSourcellvmtools>svn co http://llvm.org/svn/llvm-project/cfe/trunk clang

还记得我们前面编译的 main.c 吗?编译产生的输出,也就是中间文件 main.obj,target 文件 main.exe 都是和 main.c 在一个目录里的。

C:UsersTim.AzureADSourceReposGameEngineFromScratch>dir
 驱动器 C 中的卷是 OS
 卷的序列号是 38A2-CBDD

 C:UsersTim.AzureADSourceReposGameEngineFromScratch 的目录

2017/08/18  09:31              .
2017/08/18  09:31              ..
2017/08/18  08:30               302 .gitignore
2017/08/18  08:30             1,088 LICENSE
2017/08/18  09:29                71 main.c
2017/08/18  09:31            97,280 main.exe
2017/08/18  09:31             1,285 main.obj
2017/08/18  08:30               103 README.md
               6 个文件        100,129 字节
               2 个目录 883,355,103,232 可用字节

对于简单的程序我们可以这样。对于大型软件来说,如果我们这样编译,成千上万的中间文件会把整个目录搞得混乱不堪,非常不利于管理。最为关键的是,如果我们的代码支持一些编译选项,可以从一套代码里编译出不同的版本(比如最常见的,Debug 版和 Release 版),那么不同编译选项编译所生成的中间文件就会相互覆盖,最后搞得编译器也弄不清楚哪些文件编译过,是怎么编译的(按照什么选项编译的)。在我们码农的日常当中,如果我们遇到了一个项目第一次编译得过,第二次开始就出错,有的时候 clean 了重新编译也没用,那么多半就是这个原因了。

这种编译方式老外叫做"build in the (source) tree",这是不良的习惯。我们应该改掉。推荐的是“build outside the (source) tree”

所以让我们从 llvmtools 这个目录出去,然后建立一个 build 目录,专门用来保存编译过程当中生成的文件。

C:UsersTim.AzureADSourcellvmtools>cd ....

C:UsersTim.AzureADSource>mkdir build

C:UsersTim.AzureADSource>cd build

C:UsersTim.AzureADSourcebuild>

因为我们现在电脑上还只有 Visual Studio 所提供的编译工具,所以我们需要使用 CMake 工具来生成 Visual Studio 所需的 Solution 文件和 Project 文件,以便使用 Visual Studio 来编译 LLVM

C:UsersTim.AzureADSourcebuild>cmake -G "Visual Studio 15" ..llvm

-G "Visual Studio 15" 表示生成 Visual Studio 2017 用的项目文件。为什么叫"Visual Studio 15",这是因为在 Visual Studio 6 之后,微软改变了产品命名方式,Visual Studio 7叫Visual Studio .NET 了。后面的版本更是,一会儿差一年一会儿差两年的。但是实际上他们内部仍然继续着这个序号,证据就是你看 Windows 里面的注册表当中的信息,就知道这个序号仍然在继续。(Office 也是类似)

所以,从6开始数,Visual Studio 2017 正好是15,Visual Studio 2015 是 14,Visual Studio 2013 则是 12。(嗯?13呢?被吃掉了?估计是13这个数字风水不好。。。)

如果记不住,可以用 cmake --help 命令查看:

C:UsersTim.AzureADSourceReposGameEngineFromScratch>cmake --help
Usage

  cmake [options] 
  cmake [options] 

Specify a source directory to (re-)generate a build system for it in the
current working directory.  Specify an existing build directory to
re-generate its build system.

Options
  -C            = Pre-load a script to populate the cache.
  -D [:]=    = Create a cmake cache entry.
  -U            = Remove matching entries from CMake cache.
  -G           = Specify a build system generator.
  -T             = Specify toolset name if supported by
                                 generator.
  -A            = Specify platform name if supported by
                                 generator.
  -Wdev                        = Enable developer warnings.
  -Wno-dev                     = Suppress developer warnings.
  -Werror=dev                  = Make developer warnings errors.
  -Wno-error=dev               = Make developer warnings not errors.
  -Wdeprecated                 = Enable deprecation warnings.
  -Wno-deprecated              = Suppress deprecation warnings.
  -Werror=deprecated           = Make deprecated macro and function warnings
                                 errors.
  -Wno-error=deprecated        = Make deprecated macro and function warnings
                                 not errors.
  -E                           = CMake command mode.
  -L[A][H]                     = List non-advanced cached variables.
  --build                 = Build a CMake-generated project binary tree.
  -N                           = View mode only.
  -P                     = Process script mode.
  --find-package               = Run in pkg-config like mode.
  --graphviz=[file]            = Generate graphviz of dependencies, see
                                 CMakeGraphVizOptions.cmake for more.
  --system-information [file]  = Dump information about this system.
  --debug-trycompile           = Do not delete the try_compile build tree.
                                 Only useful on one try_compile at a time.
  --debug-output               = Put cmake in a debug mode.
  --trace                      = Put cmake in trace mode.
  --trace-expand               = Put cmake in trace mode with variable
                                 expansion.
  --trace-source=        = Trace only this CMake file/module.  Multiple
                                 options allowed.
  --warn-uninitialized         = Warn about uninitialized values.
  --warn-unused-vars           = Warn about unused variables.
  --no-warn-unused-cli         = Don't warn about command line options.
  --check-system-vars          = Find problems with variable usage in system
                                 files.
  --help,-help,-usage,-h,-H,/? = Print usage information and exit.
  --version,-version,/V []  = Print version number and exit.
  --help-full []            = Print all help manuals and exit.
  --help-manual  []    = Print one help manual and exit.
  --help-manual-list []     = List help manuals available and exit.
  --help-command  []   = Print help for one command and exit.
  --help-command-list []    = List commands with help available and exit.
  --help-commands []        = Print cmake-commands manual and exit.
  --help-module  []    = Print help for one module and exit.
  --help-module-list []     = List modules with help available and exit.
  --help-modules []         = Print cmake-modules manual and exit.
  --help-policy  []    = Print help for one policy and exit.
  --help-policy-list []     = List policies with help available and exit.
  --help-policies []        = Print cmake-policies manual and exit.
  --help-property  [] = Print help for one property and exit.
  --help-property-list []   = List properties with help available and
                                 exit.
  --help-properties []      = Print cmake-properties manual and exit.
  --help-variable var []    = Print help for one variable and exit.
  --help-variable-list []   = List variables with help available and exit.
  --help-variables []       = Print cmake-variables manual and exit.

Generators

The following generators are available on this platform:
  Visual Studio 15 2017 [arch] = Generates Visual Studio 2017 project files.
                                 Optional [arch] can be "Win64" or "ARM".
  Visual Studio 14 2015 [arch] = Generates Visual Studio 2015 project files.
                                 Optional [arch] can be "Win64" or "ARM".
  Visual Studio 12 2013 [arch] = Generates Visual Studio 2013 project files.
                                 Optional [arch] can be "Win64" or "ARM".
  Visual Studio 11 2012 [arch] = Generates Visual Studio 2012 project files.
                                 Optional [arch] can be "Win64" or "ARM".
  Visual Studio 10 2010 [arch] = Generates Visual Studio 2010 project files.
                                 Optional [arch] can be "Win64" or "IA64".
  Visual Studio 9 2008 [arch]  = Generates Visual Studio 2008 project files.
                                 Optional [arch] can be "Win64" or "IA64".
  Visual Studio 8 2005 [arch]  = Deprecated.  Generates Visual Studio 2005
                                 project files.  Optional [arch] can be
                                 "Win64".
  Borland Makefiles            = Generates Borland makefiles.
  NMake Makefiles              = Generates NMake makefiles.
  NMake Makefiles JOM          = Generates JOM makefiles.
  Green Hills MULTI            = Generates Green Hills MULTI files
                                 (experimental, work-in-progress).
  MSYS Makefiles               = Generates MSYS makefiles.
  MinGW Makefiles              = Generates a make file for use with
                                 mingw32-make.
  Unix Makefiles               = Generates standard UNIX makefiles.
  Ninja                        = Generates build.ninja files.
  Watcom WMake                 = Generates Watcom WMake makefiles.
  CodeBlocks - MinGW Makefiles = Generates CodeBlocks project files.
  CodeBlocks - NMake Makefiles = Generates CodeBlocks project files.
  CodeBlocks - NMake Makefiles JOM
                               = Generates CodeBlocks project files.
  CodeBlocks - Ninja           = Generates CodeBlocks project files.
  CodeBlocks - Unix Makefiles  = Generates CodeBlocks project files.
  CodeLite - MinGW Makefiles   = Generates CodeLite project files.
  CodeLite - NMake Makefiles   = Generates CodeLite project files.
  CodeLite - Ninja             = Generates CodeLite project files.
  CodeLite - Unix Makefiles    = Generates CodeLite project files.
  Sublime Text 2 - MinGW Makefiles
                               = Generates Sublime Text 2 project files.
  Sublime Text 2 - NMake Makefiles
                               = Generates Sublime Text 2 project files.
  Sublime Text 2 - Ninja       = Generates Sublime Text 2 project files.
  Sublime Text 2 - Unix Makefiles
                               = Generates Sublime Text 2 project files.
  Kate - MinGW Makefiles       = Generates Kate project files.
  Kate - NMake Makefiles       = Generates Kate project files.
  Kate - Ninja                 = Generates Kate project files.
  Kate - Unix Makefiles        = Generates Kate project files.
  Eclipse CDT4 - NMake Makefiles
                               = Generates Eclipse CDT 4.0 project files.
  Eclipse CDT4 - MinGW Makefiles
                               = Generates Eclipse CDT 4.0 project files.
  Eclipse CDT4 - Ninja         = Generates Eclipse CDT 4.0 project files.
  Eclipse CDT4 - Unix Makefiles= Generates Eclipse CDT 4.0 project files.

好了,然后用下面的命令build生成的Solution。 (注意要在Visual Studio的命令行里面。也就是开始菜单里面的Developer Command Prompt)当然你也可以双击LLVM.sln打开Visual Studio的IDE进行编译。效果其实一样的。

C:UsersTim.AzureADSourcebuild>msbuild LLVM.sln

这个编译看机器性能。我在 i7 8 核的 SSD 机器上大概1个半小时。

编译完成之后,我们来测试我们编译出的clang是否有问题。首先我们需要将生成物的目录加入环境变量PATH,以便在命令行能够找到它:(目录请根据你的本地实际情况修改)

4

重启命令行,检查是否可以找到 clang

C:UsersTim.AzureADSource>clang -v
clang version 6.0.0 (trunk 311143)
Target: i686-pc-windows-msvc
Thread model: posix
InstalledDir: C:UsersTim.AzureADSourcebuildDebugbin
Found CUDA installation: /Program Files/NVIDIA GPU Computing Toolkit/CUDA/v8.0, version 8.0

最后一行 CUDA 是我的环境里面别的事情安装的。与目前无关。没有安装的应该看不到这一行。

然后确保我们目前是处于 LLVM 的顶级目录,就是下面有 llvm 和 build 这两个目录的那一级目录,执行下面的命令:

C:UsersTim.AzureADSource>python.exe llvmutilslitlit.py -sv --param=build_mode=Win32 --param=build_config=Debug --param=clang_site_config=buildtoolsclangtestlit.site.cfg llvmtoolsclangtest

我这里的环境是执行会失败,python 抱怨找不到一些测试用的程序。需要修改 buildtoolsclangtestlit.site.cfg

原来的版本:

## Autogenerated from C:/Users/Tim.AzureAD/Source/llvm/tools/clang/test/lit.site.cfg.in
## Do not edit!

import sys

config.llvm_src_root = "C:/Users/Tim.AzureAD/Source/llvm"
config.llvm_obj_root = "C:/Users/Tim.AzureAD/Source/build"
config.llvm_tools_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_mode)s/bin"
config.llvm_libs_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_mode)s/lib"
config.llvm_shlib_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_mode)s/bin"
config.llvm_plugin_ext = ".dll"
config.lit_tools_dir = ""
config.clang_obj_root = "C:/Users/Tim.AzureAD/Source/build/tools/clang"
config.clang_src_dir = "C:/Users/Tim.AzureAD/Source/llvm/tools/clang"
config.clang_tools_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_mode)s/bin"
config.host_triple = "i686-pc-win32"
config.target_triple = "i686-pc-win32"
config.llvm_use_sanitizer = ""
config.have_zlib = 0
config.clang_arcmt = 1
config.clang_default_cxx_stdlib = ""
config.clang_staticanalyzer = 1
config.clang_staticanalyzer_z3 = ""
config.clang_examples = 0
config.enable_shared = 0
config.enable_backtrace = 1
config.host_arch = "AMD64"
config.enable_abi_breaking_checks = ""

改为

## Autogenerated from C:/Users/Tim.AzureAD/Source/llvm/tools/clang/test/lit.site.cfg.in
## Do not edit!

import sys

config.llvm_src_root = "C:/Users/Tim.AzureAD/Source/llvm"
config.llvm_obj_root = "C:/Users/Tim.AzureAD/Source/build"
config.llvm_tools_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_config)s/bin"
config.llvm_libs_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_config)s/lib"
config.llvm_shlib_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_config)s/bin"
config.llvm_plugin_ext = ".dll"
config.lit_tools_dir = ""
config.clang_obj_root = "C:/Users/Tim.AzureAD/Source/build/tools/clang"
config.clang_src_dir = "C:/Users/Tim.AzureAD/Source/llvm/tools/clang"
config.clang_tools_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_config)s/bin"
config.host_triple = "i686-pc-win32"
config.target_triple = "i686-pc-win32"
config.llvm_use_sanitizer = ""
config.have_zlib = 0
config.clang_arcmt = 1
config.clang_default_cxx_stdlib = ""
config.clang_staticanalyzer = 1
config.clang_staticanalyzer_z3 = ""
config.clang_examples = 0
config.enable_shared = 0
config.enable_backtrace = 1
config.host_arch = "AMD64"
config.enable_abi_breaking_checks = ""

就是把所有的%(build_mode)改为%(build_config)

如果是用 vim 修改,可以用“:%s/build_mode/build_config/g”这条命令一次修改完毕。

感觉上应该是不同的 Visual Studio 对于项目文件当中 Output 目录宏展开的方式不同导致的。

测试正常执行的样子是这样的:

C:UsersTim.AzureADSource>python llvmutilslitlit.py -sv --param=build_mode=Win32 --param=build_config=Debug --param=clang_site_config=buildtoolsclangtestlit.site.cfg llvmtoolsclangtest
lit.py: C:/Users/Tim.AzureAD/Source/llvm/tools/clang/test/lit.cfg:200: note: using clang: 'C:/Users/Tim.AzureAD/Source/build/Debug/bin/clang.EXE'
lit.py: C:UsersTim.AzureADSourcellvmutilslitlitdiscovery.py:190: warning: test suite 'Clang-Unit' contained no tests
-- Testing: 9208 tests, 8 threads --
********************
Testing: 0 .. 10.. 20.. 30.. 40.. 50.. 60.. 70.. 80.. 90..
Testing Time: 843.01s
********************
Failing Tests (2):
    Clang :: Driver/offloading-interoperability.c
    Clang :: Driver/openmp-offload-gpu.c

  Expected Passes    : 9077
  Expected Failures  : 24
  Unsupported Tests  : 105
  Unexpected Failures: 2

1 warning(s) in tests.

星号当中的是进度条。在 i7 8 核心的机器上大约需要10分钟左右。

我这里执行的过程当中出现一些 CUDA 相关的错误,应该是版本不匹配(我的是CUDA 8.0,比较新)导致,可以无视。

用新的 Toolchain 编译我们的 Hello Engine

C:UsersTim.AzureADSourceReposGameEngineFromScratch>clang main.c
main.c:3:1: warning: return type of 'main' is not 'int' [-Wmain-return-type]
void main() {
^
main.c:3:1: note: change return type to 'int'
void main() {
^~~~
int
1 warning generated.

可以看到,在 Visual Studio 下面编译完全没有问题的代码,在 clang 下面出现了 warning。所以,我们需要尽早摆脱微软的安乐窝。(*^_^*)

参考引用

  1. Clang - Getting Started

5