Vulkan 教程 Go 语言实现

401 阅读4分钟

小时候曾经多么向往自己写游戏!看到一个很新(2023.4 更新)且完整的 Vulkan 教程,刚好这段时间在学 Go 语言,打算实操一下。原教程是 C++,而 Vulkan API 为 C,所以我将透过 cgo 调用 Vulkan。

教程地址是:vulkan-tutorial.com

窗口系统我用 SDL2。就 GO, SDL, Vulkan 而言,我都是新手;) 很久以前学过一点点 OpenGL 皮毛,没有深入,大概还有一点变换矩阵的模糊印象吧。总之经过好一番折腾,才把教程中第一个 Vulkan 测试程序跑通。

Vulkan 这个词,似乎源于另一个单词 Vulcan,好像是欧洲神话中和火有关的神。

很显然需要 C 编译工具,我用 Msys2 所提供的 Mingw32-w64。SDL2 提供针对 Mingw32-w64 的开发包(头文件+静态/动态库),下载即用。另外 Msys2 提供 Vulkan 开发包。所以,开发工具的准备工作十分简单。

具体地,Vulkan 相关开发包有 2 个:

pacman -S vulkan-devel shaderc

将 Mingw32-w64 bin 目录添加到 PATH 环境变量中。此外还需设置另一个环境变量 CGO_LDFLAGS_ALLOW,以 Windows 命令行为例:

set CGO_LDFLAGS_ALLOW=.+

变量值是一个正则表达式,这里 .+ 表示任意非空字符串。cgo 默认允许的 CFLAGSLDFLAGS 仅限于 -I, -D, -L, -l 这 4 个。如果编译时需要指定其他选项,则须通过 CGO_CFLAGS_ALLOWCGO_LDFLAGS_ALLOW 环境变量进行显式设定。

基本上重要的事就是这些了。程序很简单,打开一个窗口,检查本机 Vulkan 支持的扩展(Extension)个数,源码如下:

// vk_testing.go

package main

import (
	"fmt"
	"runtime"

	"example.com/vk_tutor/sdl2"
	"example.com/vk_tutor/vulkan"
)

func main() {

	runtime.LockOSThread()

	sdl2.SDL_SetMainReady()
	sdl2.SDL_Init(sdl2.SDL_INIT_EVERYTHING)

	var window = sdl2.SDL_CreateWindow("Vulkan window", 100, 100, 640, 480, sdl2.SDL_WINDOW_SHOWN|sdl2.SDL_WINDOW_VULKAN)

	var ext_cnt uint32
	vulkan.VkEnumerateInstanceExtensionProperties(nil, &ext_cnt, nil)
	fmt.Printf("%v extensions supported\n", ext_cnt)

	untilQuit()

	sdl2.SDL_DestroyWindow(window)
	sdl2.SDL_Quit()
}

func untilQuit() {

	var e sdl2.SDL_Event

loop:
	for {
		if 0 == sdl2.SDL_PollEvent(&e) {
			continue
		}

		switch e.Type() {
		case sdl2.SDL_QUIT:
			break loop
		} // switch
	} // for
}

值得一说的是runtime.LockOSThread()。Go 的多任务机制(Goroutine)与 C 的多线程(例如 pthreads)差别太大了。Go 运行时内部维护着一个线程池,一个 Goroutine 在运行时在不同时间可能被分配到不同的线程上。当通过 cgo 调用 C 库时,如果只是调用 Re-entrant 函数,这个毫无问题;但是如果 C 的程序流程牵涉到 Thread local 状态,那就与 Go 的 Coroutine 调度机制不相容了。调用runtime.LockOSThread(),将当前的 Goroutine “锁定”到当前线程,即:此 Goroutine 将独占、且只在当前线程执行(直至调用runtime.UnlockOSThread())。

我不确定 SDL 和 Vulkan 的内部实现为何,安全起见,调用runtime.LockOSThread()锁定线程。

在我的十年老本上,测试程序打印内容显示有 11 个 Vulkan 扩展--说实话,在下目前尚不清楚此为何物。查看设备管理器有 2 块显卡,分别是“AMD Radeon R5 M200 Series”和“Intel HD Graphics 5500”,应该是一集成显卡+一独立显卡。不懂硬件配置,考虑到电脑的年头和档次,应该不是什么高端的了。我估计,对 Vulkan 的支持取决于显卡驱动程序。我看驱动程序都是微软提供的,日期分别是2020、2016 年。

关于显卡硬件或驱动程序的 Vulkan 支持,还是一个稀里糊涂的状态。

sdl2vulkan 这 2 个包是 cgo 封装层。这里基本上就是机械地一对一地映射,纯体力活。下面放一点 sdl2的代码片段:

package sdl2

// #cgo CFLAGS: -I${SRCDIR}/include -I${SRCDIR}/include/SDL2 -DSDL_MAIN_HANDLED
// #cgo LDFLAGS: -L${SRCDIR}/lib -static -lmingw32 -lSDL2main -lSDL2 -mwindows
// #cgo LDFLAGS: -Wl,--dynamicbase -Wl,--nxcompat -Wl,--high-entropy-va -lm -ldinput8 -ldxguid -ldxerr8 -luser32 -lgdi32 -lwinmm -limm32 -lole32 -loleaut32 -lshell32 -lsetupapi -lversion -luuid
// #include "SDL.h"
import "C"

const (
	SDL_INIT_TIMER          uint32 = C.SDL_INIT_TIMER
	SDL_INIT_AUDIO          uint32 = C.SDL_INIT_AUDIO
	SDL_INIT_VIDEO          uint32 = C.SDL_INIT_VIDEO    /**< SDL_INIT_VIDEO implies SDL_INIT_EVENTS */
	SDL_INIT_JOYSTICK       uint32 = C.SDL_INIT_JOYSTICK /**< SDL_INIT_JOYSTICK implies SDL_INIT_EVENTS */
	SDL_INIT_HAPTIC         uint32 = C.SDL_INIT_HAPTIC
	SDL_INIT_GAMECONTROLLER uint32 = C.SDL_INIT_GAMECONTROLLER /**< SDL_INIT_GAMECONTROLLER implies SDL_INIT_JOYSTICK */
	SDL_INIT_EVENTS         uint32 = C.SDL_INIT_EVENTS
	SDL_INIT_SENSOR         uint32 = C.SDL_INIT_SENSOR
	SDL_INIT_NOPARACHUTE    uint32 = C.SDL_INIT_NOPARACHUTE /**< compatibility; this flag is ignored. */
	SDL_INIT_EVERYTHING     uint32 = C.SDL_INIT_EVERYTHING
)

// extern DECLSPEC int SDLCALL SDL_Init(Uint32 flags);
func SDL_Init(flags uint32) int {
	return int(C.SDL_Init(C.Uint32(flags)))
}

...

SDL2 编译选项-DSDL_MAIN_HANDLED要作个说明。SDL 库中已提供程序入口,在 Windows 平台上就是WinMain()函数;SDL 提供的入口函数将会调用 client 端定义的main()函数(事实上,main()函数通过宏定义,实际名称为SDL_main())。如果要绕过上述默认行为,则须在#include "SDL.h"前定义SDL_MAIN_HANDLED宏,并且在调用SDL_Init()之前先调用SDL_SetMainReady()。这里我们的程序入口是 Go main()函数,故然。

待续。