欢迎光临
我们一直在努力

Using Vulkan -- Windowing, Audio, and Input

本章提供用于将GLFW、SDL2等库以及原生平台API集成到Vulkan应用中的样板代码,用于窗口创建、输入处理。

概述

Vulkan本身是图形与计算API,但大多数应用需要与操作系统交互以创建窗口、处理用户输入,以及可能的音频处理。本章介绍开发Vulkan应用时,完成这些任务最常用的库与方案。


窗口创建

GLFW

GLFW是轻量跨平台库,用于创建窗口、上下文与surface,接收输入与事件。因其简洁API与内置Vulkan支持,在Vulkan开发中尤为流行。

使用 GLFW 搭建 Vulkan

#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#include <iostream>

int main() {
// 初始化 GLFW
if (!glfwInit()) {
std::cerr << "Failed to initialize GLFW" << std::endl;
return -1;
}

// GLFW 最初设计用于创建 OpenGL 上下文,因此需要告知它不创建
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);

// 创建窗口
GLFWwindow* window = glfwCreateWindow(800, 600, "Vulkan Window", nullptr, nullptr);
if (!window) {
std::cerr << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}

// 创建 Vulkan instance(未展示)
VkInstance instance = VK_NULL_HANDLE;
// … create instance …

// 创建 Vulkan surface
VkSurfaceKHR surface;
VkResult result = glfwCreateWindowSurface(instance, window, nullptr, &surface);
if (result != VK_SUCCESS) {
std::cerr << "Failed to create window surface" << std::endl;
return -1;
}

// 主循环
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();

// 使用 Vulkan 渲染(未展示)
}

// 清理
vkDestroySurfaceKHR(instance, surface, nullptr);
glfwDestroyWindow(window);
glfwTerminate();

return 0;
}

GLFW 输入处理

GLFW提供轮询与回调两种输入处理方式:

// 键盘输入回调
void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) {
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) {
glfwSetWindowShouldClose(window, GLFW_TRUE);
}
}

// 鼠标移动回调
void cursorPositionCallback(GLFWwindow* window, double xpos, double ypos) {
// 处理鼠标移动
std::cout << "Mouse position: " << xpos << ", " << ypos << std::endl;
}

// 鼠标按键回调
void mouseButtonCallback(GLFWwindow* window, int button, int action, int mods) {
if (button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_PRESS) {
// 处理左键按下
std::cout << "Left mouse button pressed" << std::endl;
}
}

// 主函数中注册回调:
glfwSetKeyCallback(window, keyCallback);
glfwSetCursorPosCallback(window, cursorPositionCallback);
glfwSetMouseButtonCallback(window, mouseButtonCallback);

// 或在主循环中轮询输入:
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) {
// 向前移动
}


SDL2

SDL2(Simple DirectMedia Layer)是跨平台开发库,提供对音频、键盘、鼠标、手柄与图形硬件的底层访问。相比GLFW更全面,自带音频支持与更多输入选项。

使用 SDL2 搭建 Vulkan

#include <SDL2/SDL.h>
#include <SDL2/SDL_vulkan.h>
#include <vulkan/vulkan.h>
#include <iostream>
#include <vector>

int main() {
// 初始化 SDL
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) != 0) {
std::cerr << "SDL_Init Error: " << SDL_GetError() << std::endl;
return -1;
}

// 创建支持 Vulkan 的窗口
SDL_Window* window = SDL_CreateWindow(
"Vulkan SDL2 Window",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
800, 600,
SDL_WINDOW_VULKAN | SDL_WINDOW_SHOWN
);

if (!window) {
std::cerr << "SDL_CreateWindow Error: " << SDL_GetError() << std::endl;
SDL_Quit();
return -1;
}

// 获取 SDL 所需的 Vulkan 扩展
unsigned int extensionCount;
if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, nullptr)) {
std::cerr << "Failed to get Vulkan extension count" << std::endl;
return -1;
}

std::vector<const char*> extensions(extensionCount);
if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, extensions.data())) {
std::cerr << "Failed to get Vulkan extensions" << std::endl;
return -1;
}

// 创建 Vulkan instance(未展示)
VkInstance instance = VK_NULL_HANDLE;
// … create instance with extensions …

// 创建 Vulkan surface
VkSurfaceKHR surface;
if (!SDL_Vulkan_CreateSurface(window, instance, &surface)) {
std::cerr << "Failed to create Vulkan surface" << std::endl;
return -1;
}

// 主循环
bool running = true;
SDL_Event event;
while (running) {
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
running = false;
}
}

// 使用 Vulkan 渲染(未展示)
}

// 清理
vkDestroySurfaceKHR(instance, surface, nullptr);
SDL_DestroyWindow(window);
SDL_Quit();

return 0;
}

SDL2 输入处理

SDL2使用事件系统处理输入:

// 主循环中
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_QUIT:
running = false;
break;
case SDL_KEYDOWN:
if (event.key.keysym.sym == SDLK_ESCAPE) {
running = false;
}
if (event.key.keysym.sym == SDLK_w) {
// 向前移动
}
break;
case SDL_MOUSEMOTION:
std::cout << "Mouse position: " << event.motion.x << ", " << event.motion.y << std::endl;
break;
case SDL_MOUSEBUTTONDOWN:
if (event.button.button == SDL_BUTTON_LEFT) {
std::cout << "Left mouse button pressed" << std::endl;
}
break;
}
}

// 或获取键盘状态
const Uint8* keyboardState = SDL_GetKeyboardState(NULL);
if (keyboardState[SDL_SCANCODE_W]) {
// 向前移动
}

SDL2 音频集成

SDL2提供简单音频API:

SDL_PauseAudioDevice(audioDevice, 0);

// 后续结束时:
SDL_CloseAudioDevice(audioDevice);


SFML

SFML(Simple and Fast Multimedia Library)是跨平台C++库,提供简洁接口访问多媒体组件(输入、音频、图形)。相比GLFW功能更丰富,支持移动平台等更多平台。

使用 SFML 搭建 Vulkan

#include <SFML/Window.hpp>
#include <vulkan/vulkan.h>
#include <iostream>
#include <vector>

int main() {
// 创建 SFML 窗口
sf::WindowBase window{sf::VideoMode({800, 600}), "Vulkan SFML Window", sf::Style::Default};

// 获取 SFML 所需的 Vulkan 扩展
std::vector<const char*> extensions = sf::Vulkan::getGraphicsRequiredInstanceExtensions();

// 创建 Vulkan instance(未展示)
VkInstance instance = VK_NULL_HANDLE;
// … create instance with extensions …

// 创建 Vulkan surface
VkSurfaceKHR surface;
if (!window.createVulkanSurface(instance, surface)) {
std::cerr << "Failed to create Vulkan surface" << std::endl;
return -1;
}

// 主循环
while (window.isOpen()) {
while (const std::optional event = window.pollEvent()) {
if (event->is<sf::Event::Closed>()) {
window.close();
}
}

// 使用 Vulkan 渲染(未展示)
}

vkDestroySurfaceKHR(instance, surface, nullptr);
// SFML 无需显式清理

return 0;
}

SFML 输入处理

// 主循环中
while (window.isOpen()) {
while (const std::optional event = window.pollEvent()) {
if (event->is<sf::Event::Closed>()) {
window.close();
}
if (event->is<sf::Event::KeyPressed>()) {
if (event->getIf<sf::Event::KeyPressed>()->code == sf::Keyboard::Key::W) {
// 向前移动
}
}
if (event->is<sf::Event::MouseButtonPressed>()) {
if (event->getIf<sf::Event::MouseButtonPressed>()->button == sf::Mouse::Button::Left) {
// 拾取物体
}
}
}
}

// 或轮询按键状态
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::W)) {
// 向前移动
}

SFML 音频集成

SFML提供简洁的音频与音乐API:

// 从 wav 文件加载声音缓冲
const sf::SoundBuffer buffer("soundfile.wav");
// 用缓冲创建声音实例
sf::Sound sound(buffer);
// 播放
sound.play();

// 加载音乐轨道
sf::Music music("soundtrack.ogg");
// 播放
music.play();


原生平台 API

需要更直接控制或平台特有功能时,可使用原生API创建窗口与处理输入。

Windows(Win32)

#define VK_USE_PLATFORM_WIN32_KHR
#include <vulkan/vulkan.h>
#include <windows.h>
#include <iostream>

// 窗口过程
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_CLOSE:
PostQuitMessage(0);
return 0;
case WM_KEYDOWN:
if (wParam == VK_ESCAPE) {
PostQuitMessage(0);
}
return 0;
case WM_LBUTTONDOWN:
std::cout << "Left mouse button pressed" << std::endl;
return 0;
case WM_MOUSEMOVE:
std::cout << "Mouse position: " << LOWORD(lParam) << ", " << HIWORD(lParam) << std::endl;
return 0;
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR pCmdLine, int nCmdShow) {
// 注册窗口类
WNDCLASSEX wc = {};
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.lpszClassName = "VulkanWindowClass";
RegisterClassEx(&wc);

// 创建窗口
HWND hwnd = CreateWindowEx(
0,
"VulkanWindowClass",
"Vulkan Win32 Window",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
800, 600,
NULL,
NULL,
hInstance,
NULL
);

if (!hwnd) {
std::cerr << "Failed to create window" << std::endl;
return -1;
}

ShowWindow(hwnd, nCmdShow);

// 创建 Vulkan instance(未展示)
VkInstance instance = VK_NULL_HANDLE;
// … create instance …

// 创建 Vulkan surface
VkWin32SurfaceCreateInfoKHR createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
createInfo.hwnd = hwnd;
createInfo.hinstance = hInstance;

VkSurfaceKHR surface;
VkResult result = vkCreateWin32SurfaceKHR(instance, &createInfo, nullptr, &surface);
if (result != VK_SUCCESS) {
std::cerr << "Failed to create window surface" << std::endl;
return -1;
}

// 主循环
MSG msg = {};
bool running = true;
while (running) {
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
running = false;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}

// 使用 Vulkan 渲染(未展示)
}

// 清理
vkDestroySurfaceKHR(instance, surface, nullptr);
DestroyWindow(hwnd);

return 0;
}

Linux(XCB)

#define VK_USE_PLATFORM_XCB_KHR
#include <vulkan/vulkan.h>
#include <xcb/xcb.h>
#include <iostream>

int main() {
// 连接 X 服务器
xcb_connection_t* connection = xcb_connect(NULL, NULL);
if (xcb_connection_has_error(connection)) {
std::cerr << "Failed to connect to X server" << std::endl;
return -1;
}

// 获取屏幕
const xcb_setup_t* setup = xcb_get_setup(connection);
xcb_screen_iterator_t iter = xcb_setup_roots_iterator(setup);
xcb_screen_t* screen = iter.data;

// 创建窗口
xcb_window_t window = xcb_generate_id(connection);
uint32_t value_mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK;
uint32_t value_list[2] = {
screen->black_pixel,
XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_POINTER_MOTION | XCB_EVENT_MASK_STRUCTURE_NOTIFY
};

xcb_create_window(
connection,
XCB_COPY_FROM_PARENT,
window,
screen->root,
0, 0,
800, 600,
0,
XCB_WINDOW_CLASS_INPUT_OUTPUT,
screen->root_visual,
value_mask,
value_list
);

// 设置窗口标题
xcb_change_property(
connection,
XCB_PROP_MODE_REPLACE,
window,
XCB_ATOM_WM_NAME,
XCB_ATOM_STRING,
8,
13,
"Vulkan Window"
);

// 映射窗口
xcb_map_window(connection, window);
xcb_flush(connection);

// 创建 Vulkan instance(未展示)
VkInstance instance = VK_NULL_HANDLE;
// … create instance …

// 创建 Vulkan surface
VkXcbSurfaceCreateInfoKHR createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_XCB_SURFACE_CREATE_INFO_KHR;
createInfo.connection = connection;
createInfo.window = window;

VkSurfaceKHR surface;
VkResult result = vkCreateXcbSurfaceKHR(instance, &createInfo, nullptr, &surface);
if (result != VK_SUCCESS) {
std::cerr << "Failed to create window surface" << std::endl;
return -1;
}

// 主循环
bool running = true;
while (running) {
xcb_generic_event_t* event;
while ((event = xcb_poll_for_event(connection))) {
switch (event->response_type & 0x7f) {
case XCB_CLIENT_MESSAGE:
running = false;
break;
case XCB_KEY_PRESS: {
xcb_key_press_event_t* keyEvent = (xcb_key_press_event_t*)event;
// 处理按键
break;
}
case XCB_BUTTON_PRESS: {
xcb_button_press_event_t* buttonEvent = (xcb_button_press_event_t*)event;
// 处理按键
break;
}
case XCB_MOTION_NOTIFY: {
xcb_motion_notify_event_t* motionEvent = (xcb_motion_notify_event_t*)event;
// 处理鼠标移动
break;
}
}
free(event);
}

// 使用 Vulkan 渲染(未展示)
}

// 清理
vkDestroySurfaceKHR(instance, surface, nullptr);
xcb_destroy_window(connection, window);
xcb_disconnect(connection);

return 0;
}

Linux(Wayland)

Wayland是面向Linux的现代显示服务协议,旨在替代X Window System,为图形应用提供更简洁、高效、安全的架构。

#define VK_USE_PLATFORM_WAYLAND_KHR
#include <vulkan/vulkan.h>
#include <wayland-client.h>
#include <iostream>
#include <cstring>

// Wayland 协议监听器
struct WaylandData {
wl_display* display;
wl_registry* registry;
wl_compositor* compositor;
wl_shell* shell;
wl_surface* surface;
wl_shell_surface* shellSurface;
bool running;
};

// 注册表监听器回调
static void registry_global(void* data, wl_registry* registry, uint32_t id, const char* interface, uint32_t version) {
WaylandData* waylandData = static_cast<WaylandData*>(data);

if (strcmp(interface, "wl_compositor") == 0) {
waylandData->compositor = static_cast<wl_compositor*>(
wl_registry_bind(registry, id, &wl_compositor_interface, 1)
);
} else if (strcmp(interface, "wl_shell") == 0) {
waylandData->shell = static_cast<wl_shell*>(
wl_registry_bind(registry, id, &wl_shell_interface, 1)
);
}
}

static void registry_global_remove(void* data, wl_registry* registry, uint32_t name) {
// 处理全局对象移除
}

static const wl_registry_listener registry_listener = {
registry_global,
registry_global_remove
};

// Shell surface 监听器回调
static void shell_surface_ping(void* data, wl_shell_surface* shell_surface, uint32_t serial) {
wl_shell_surface_pong(shell_surface, serial);
}

static void shell_surface_configure(void* data, wl_shell_surface* shell_surface, uint32_t edges, int32_t width, int32_t height) {
// 处理大小调整
}

static void shell_surface_popup_done(void* data, wl_shell_surface* shell_surface) {
// 处理弹出结束
}

static const wl_shell_surface_listener shell_surface_listener = {
shell_surface_ping,
shell_surface_configure,
shell_surface_popup_done
};

int main() {
WaylandData waylandData = {};

// 连接 Wayland 显示服务
waylandData.display = wl_display_connect(nullptr);
if (!waylandData.display) {
std::cerr << "Failed to connect to Wayland display" << std::endl;
return -1;
}

// 获取注册表
waylandData.registry = wl_display_get_registry(waylandData.display);
wl_registry_add_listener(waylandData.registry, &registry_listener, &waylandData);

// 等待注册表事件
wl_display_roundtrip(waylandData.display);

// 检查是否获取必需全局对象
if (!waylandData.compositor || !waylandData.shell) {
std::cerr << "Failed to get Wayland compositor or shell" << std::endl;
return -1;
}

// 创建 surface
waylandData.surface = wl_compositor_create_surface(waylandData.compositor);
if (!waylandData.surface) {
std::cerr << "Failed to create Wayland surface" << std::endl;
return -1;
}

// 创建 shell surface
waylandData.shellSurface = wl_shell_get_shell_surface(waylandData.shell, waylandData.surface);
if (!waylandData.shellSurface) {
std::cerr << "Failed to create Wayland shell surface" << std::endl;
return -1;
}

// 设置 shell surface
wl_shell_surface_add_listener(waylandData.shellSurface, &shell_surface_listener, &waylandData);
wl_shell_surface_set_toplevel(waylandData.shellSurface);
wl_shell_surface_set_title(waylandData.shellSurface, "Vulkan Wayland Window");

// 创建 Vulkan instance(未展示)
VkInstance instance = VK_NULL_HANDLE;
// … create instance with VK_KHR_wayland_surface extension …

// 创建 Vulkan surface
VkWaylandSurfaceCreateInfoKHR createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR;
createInfo.display = waylandData.display;
createInfo.surface = waylandData.surface;

VkSurfaceKHR surface;
VkResult result = vkCreateWaylandSurfaceKHR(instance, &createInfo, nullptr, &surface);
if (result != VK_SUCCESS) {
std::cerr << "Failed to create Wayland surface for Vulkan" << std::endl;
return -1;
}

// 主循环
waylandData.running = true;
while (waylandData.running) {
// 处理 Wayland 事件
wl_display_dispatch_pending(waylandData.display);

// 使用 Vulkan 渲染(未展示)

// 刷新 Wayland 命令
wl_display_flush(waylandData.display);
}

// 清理
vkDestroySurfaceKHR(instance, surface, nullptr);

if (waylandData.shellSurface) {
wl_shell_surface_destroy(waylandData.shellSurface);
}

if (waylandData.surface) {
wl_surface_destroy(waylandData.surface);
}

if (waylandData.shell) {
wl_shell_destroy(waylandData.shell);
}

if (waylandData.compositor) {
wl_compositor_destroy(waylandData.compositor);
}

if (waylandData.registry) {
wl_registry_destroy(waylandData.registry);
}

if (waylandData.display) {
wl_display_disconnect(waylandData.display);
}

return 0;
}

macOS(Cocoa)

Cocoa是苹果面向macOS应用开发的原生面向对象API。macOS上的Vulkan应用通常使用MoltenVK,将Vulkan调用翻译为Metal。

[NSApp run];

// 清理(通常不会执行到,应用由 Cocoa 终止)
vkDestroySurfaceKHR(instance, surface, nullptr);
}

return 0;
}

iOS(UIKit)

UIKit是苹果用于构建iOS应用界面的框架。与macOS类似,iOS上的Vulkan应用通常使用MoltenVK。

#define VK_USE_PLATFORM_IOS_MVK
#include <vulkan/vulkan.h>
#include <UIKit/UIKit.h>
#include <iostream>

// 用于 Vulkan 渲染的 UIView 子类
@interface VulkanView : UIView
@end

@implementation VulkanView
+ (Class)layerClass {
return [CAMetalLayer class];
}
@end

// Vulkan 视图控制器
@interface VulkanViewController : UIViewController
@property (nonatomic, strong) VulkanView *vulkanView;
@property (nonatomic, assign) VkInstance instance;
@property (nonatomic, assign) VkSurfaceKHR surface;
@end

@implementation VulkanViewController

– (void)viewDidLoad {
[super viewDidLoad];

// 创建 Vulkan 视图
self.vulkanView = [[VulkanView alloc] initWithFrame:self.view.bounds];
self.vulkanView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self.view addSubview:self.vulkanView];

// 创建 Vulkan instance(未展示)
// … create instance with VK_MVK_ios_surface extension …

// 创建 Vulkan surface
VkIOSSurfaceCreateInfoMVK createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_IOS_SURFACE_CREATE_INFO_MVK;
createInfo.pView = (__bridge void*)self.vulkanView;

VkResult result = vkCreateIOSSurfaceMVK(self.instance, &createInfo, nullptr, &self.surface);
if (result != VK_SUCCESS) {
NSLog(@"Failed to create iOS surface for Vulkan");
}
}

– (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self.vulkanView];
NSLog(@"Touch began at: %f, %f", point.x, point.y);
}

– (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self.vulkanView];
NSLog(@"Touch moved to: %f, %f", point.x, point.y);
}

– (void)dealloc {
if (self.surface != VK_NULL_HANDLE) {
vkDestroySurfaceKHR(self.instance, self.surface, nullptr);
}
}
@end

// 应用委托
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@end

@implementation AppDelegate

– (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.window.rootViewController = [[VulkanViewController alloc] init];
[self.window makeKeyAndVisible];
return YES;
}
@end

int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

Android

Android是谷歌的移动操作系统。Android 7.0(API level 24)及以上原生支持Vulkan。

#define VK_USE_PLATFORM_ANDROID_KHR
#include <vulkan/vulkan.h>
#include <android/native_window.h>
#include <android_native_app_glue.h>
#include <android/log.h>

#define LOGI(…) ((void)__android_log_print(ANDROID_LOG_INFO, "VulkanApp", __VA_ARGS__))
#define LOGW(…) ((void)__android_log_print(ANDROID_LOG_WARN, "VulkanApp", __VA_ARGS__))
#define LOGE(…) ((void)__android_log_print(ANDROID_LOG_ERROR, "VulkanApp", __VA_ARGS__))

// 全局应用状态
struct AppState {
ANativeWindow* window;
VkInstance instance;
VkSurfaceKHR surface;
bool running;
};

// 处理 Android 输入事件
static int32_t handleInput(struct android_app* app, AInputEvent* event) {
AppState* appState = (AppState*)app->userData;

if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION) {
float x = AMotionEvent_getX(event, 0);
float y = AMotionEvent_getY(event, 0);

switch (AMotionEvent_getAction(event) & AMOTION_EVENT_ACTION_MASK) {
case AMOTION_EVENT_ACTION_DOWN:
LOGI("Touch down at: %f, %f", x, y);
return 1;
case AMOTION_EVENT_ACTION_MOVE:
LOGI("Touch moved to: %f, %f", x, y);
return 1;
case AMOTION_EVENT_ACTION_UP:
LOGI("Touch up at: %f, %f", x, y);
return 1;
}
} else if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_KEY) {
int32_t keyCode = AKeyEvent_getKeyCode(event);
if (keyCode == AKEYCODE_BACK) {
appState->running = false;
return 1;
}
}

return 0;
}

// 处理 Android 应用命令
static void handleCmd(struct android_app* app, int32_t cmd) {
AppState* appState = (AppState*)app->userData;

switch (cmd) {
case APP_CMD_INIT_WINDOW:
if (app->window != NULL) {
appState->window = app->window;

// 创建 Vulkan instance(未展示)
// … create instance with VK_KHR_android_surface extension …

// 创建 Vulkan surface
VkAndroidSurfaceCreateInfoKHR createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR;
createInfo.window = appState->window;

VkResult result = vkCreateAndroidSurfaceKHR(appState->instance, &createInfo, nullptr, &appState->surface);
if (result != VK_SUCCESS) {
LOGE("Failed to create Android surface for Vulkan");
}
}
break;
case APP_CMD_TERM_WINDOW:
// 窗口关闭时清理 surface
if (appState->surface != VK_NULL_HANDLE) {
vkDestroySurfaceKHR(appState->instance, appState->surface, nullptr);
appState->surface = VK_NULL_HANDLE;
}
appState->window = nullptr;
break;
case APP_CMD_GAINED_FOCUS:
// 应用获得焦点,开始渲染
break;
case APP_CMD_LOST_FOCUS:
// 应用失去焦点,停止渲染
break;
}
}

// Android 应用入口
void android_main(struct android_app* app) {
AppState appState = {};
appState.running = true;

app->userData = &appState;
app->onAppCmd = handleCmd;
app->onInputEvent = handleInput;

// 主循环
while (app->destroyRequested == 0 && appState.running) {
// 处理事件
int events;
struct android_poll_source* source;

while (ALooper_pollAll(0, nullptr, &events, (void**)&source) >= 0) {
if (source != nullptr) {
source->process(app, source);
}
}

// 使用 Vulkan 渲染(未展示)
}

// 清理
if (appState.surface != VK_NULL_HANDLE) {
vkDestroySurfaceKHR(appState.instance, appState.surface, nullptr);
}
}


音频集成

Vulkan本身不提供音频能力,可配合多种库实现音频处理。

OpenAL

OpenAL是跨平台3D音频API,用于高效渲染多通道三维定位音频。

#include <AL/al.h>
#include <AL/alc.h>
#include <iostream>
#include <vector>

bool initOpenAL() {
// 打开默认设备
ALCdevice* device = alcOpenDevice(nullptr);
if (!device) {
std::cerr << "Failed to open OpenAL device" << std::endl;
return false;
}

// 创建上下文
ALCcontext* context = alcCreateContext(device, nullptr);
if (!context) {
std::cerr << "Failed to create OpenAL context" << std::endl;
alcCloseDevice(device);
return false;
}

// 设为当前上下文
if (!alcMakeContextCurrent(context)) {
std::cerr << "Failed to make OpenAL context current" << std::endl;
alcDestroyContext(context);
alcCloseDevice(device);
return false;
}

return true;
}

void cleanupOpenAL() {
ALCcontext* context = alcGetCurrentContext();
ALCdevice* device = alcGetContextsDevice(context);

alcMakeContextCurrent(nullptr);
alcDestroyContext(context);
alcCloseDevice(device);
}

// 播放声音示例
void playSound(const std::vector<ALubyte>& audioData, ALsizei frequency) {
// 生成缓冲
ALuint buffer;
alGenBuffers(1, &buffer);

// 填充音频数据
alBufferData(buffer, AL_FORMAT_MONO8, audioData.data(), audioData.size(), frequency);

// 生成声源
ALuint source;
alGenSources(1, &source);

// 绑定缓冲到声源
alSourcei(source, AL_BUFFER, buffer);

// 播放
alSourcePlay(source);

// 等待播放结束(实际应用中处理方式不同)
ALint state;
do {
alGetSourcei(source, AL_SOURCE_STATE, &state);
} while (state == AL_PLAYING);

// 清理
alDeleteSources(1, &source);
alDeleteBuffers(1, &buffer);
}

FMOD

FMOD是商用音效引擎,广泛用于游戏与应用。

#include <fmod.hpp>
#include <fmod_errors.h>
#include <iostream>

void ERRCHECK(FMOD_RESULT result) {
if (result != FMOD_OK) {
std::cerr << "FMOD error: " << FMOD_ErrorString(result) << std::endl;
exit(-1);
}
}

int main() {
FMOD::System* system = nullptr;
FMOD::Sound* sound = nullptr;
FMOD::Channel* channel = nullptr;

// 创建 FMOD 系统
ERRCHECK(FMOD::System_Create(&system));

// 初始化 FMOD
ERRCHECK(system->init(32, FMOD_INIT_NORMAL, nullptr));

// 加载声音
ERRCHECK(system->createSound("sound.wav", FMOD_DEFAULT, nullptr, &sound));

// 播放声音
ERRCHECK(system->playSound(sound, nullptr, false, &channel));

// 主循环
bool running = true;
while (running) {
// 更新 FMOD
ERRCHECK(system->update());

// 检查是否仍在播放
bool isPlaying = false;
if (channel) {
channel->isPlaying(&isPlaying);
if (!isPlaying) {
running = false;
}
}

// 此处为 Vulkan 渲染代码
}

// 清理
ERRCHECK(sound->release());
ERRCHECK(system->close());
ERRCHECK(system->release());

return 0;
}


移动平台音频集成

移动平台自带针对设备优化的音频API,提供音频焦点、电量管理、中断处理等移动环境专属功能。

Android 音频

Android提供AAudio与已废弃的OpenSL ES用于游戏等高性能音频。为兼容99%设备,推荐Oboe库。

Oboe

Oboe是谷歌开发的C++库,为Android提供高性能低延迟音频API,是游戏与实时音频应用的推荐方案。Oboe提供统一API,自动选择最优后端:

  • Android 8.0(API 26)及以上使用AAudio
  • 旧版本回退到OpenSL ES

#include <oboe/Oboe.h>
#include <android/log.h>
#include <cmath>

#define LOGE(…) __android_log_print(ANDROID_LOG_ERROR, "OboeAudioEngine", __VA_ARGS__)

class OboeAudioEngine : public oboe::AudioStreamCallback {
public:
OboeAudioEngine() : stream_(nullptr), phase_(0.0f) {}
~OboeAudioEngine() { closeStream(); }

bool setupAudioStream() {
// 创建音频流构建器
oboe::AudioStreamBuilder builder;

// 配置构建器
builder.setDirection(oboe::Direction::Output)
->setPerformanceMode(oboe::PerformanceMode::LowLatency)
->setSharingMode(oboe::SharingMode::Exclusive)
->setFormat(oboe::AudioFormat::Float)
->setChannelCount(oboe::ChannelCount::Stereo)
->setCallback(this);

// 打开流
oboe::Result result = builder.openStream(stream_);
if (result != oboe::Result::OK) {
LOGE("Failed to create audio stream. Error: %s", oboe::convertToText(result));
return false;
}

// 从流获取采样率(若请求采样率不可用)
sampleRate_ = stream_->getSampleRate();

return true;
}

bool startStream() {
if (!stream_) {
return false;
}

oboe::Result result = stream_->requestStart();
if (result != oboe::Result::OK) {
LOGE("Failed to start audio stream. Error: %s", oboe::convertToText(result));
return false;
}

return true;
}

void stopStream() {
if (stream_) {
stream_->requestStop();
}
}

void closeStream() {
if (stream_) {
stream_->close();
stream_.reset();
}
}

// AudioStreamCallback 实现
oboe::DataCallbackResult onAudioReady(
oboe::AudioStream *stream,
void *audioData,
int32_t numFrames) override {

float *buffer = static_cast<float*>(audioData);

// 生成音频数据(简单正弦波示例)
for (int i = 0; i < numFrames * 2; i += 2) {
float sample = 0.5f * sinf(phase_);

// 写入立体声通道
buffer[i] = sample; // 左
buffer[i + 1] = sample; // 右

// 更新相位
phase_ += 2.0f * M_PI * 440.0f / sampleRate_; // 440 Hz
if (phase_ >= 2.0f * M_PI) {
phase_ -= 2.0f * M_PI;
}
}

return oboe::DataCallbackResult::Continue;
}

// 错误回调
void onErrorBeforeClose(oboe::AudioStream *stream, oboe::Result error) override {
LOGE("Oboe error before close: %s", oboe::convertToText(error));
}

void onErrorAfterClose(oboe::AudioStream *stream, oboe::Result error) override {
LOGE("Oboe error after close: %s", oboe::convertToText(error));

// 断开时重新打开流(如拔耳机)
if (error == oboe::Result::ErrorDisconnected) {
closeStream();
setupAudioStream();
startStream();
}
}

private:
std::shared_ptr<oboe::AudioStream> stream_;
float phase_;
int32_t sampleRate_;
};

// 使用方式:
// OboeAudioEngine audioEngine;
// audioEngine.setupAudioStream();
// audioEngine.startStream();
//
// 结束:
// audioEngine.stopStream();
// audioEngine.closeStream();

Android 音频焦点

处理音频焦点对Android用户体验至关重要:

// 原生代码需通过 JNI 调用 Java 方法
extern "C" {
JNIEXPORT void JNICALL
Java_com_example_vulkanaudio_AudioManager_nativeOnAudioFocusGained(JNIEnv *env, jobject thiz) {
// 恢复播放
// 示例:
// audioEngine->start();
}

JNIEXPORT void JNICALL
Java_com_example_vulkanaudio_AudioManager_nativeOnAudioFocusLost(JNIEnv *env, jobject thiz) {
// 暂停播放
// 示例:
// audioEngine->stop();
}
}

Java端:

nativeOnAudioFocusGained();
}
}

// 原生方法
private native void nativeOnAudioFocusGained();
private native void nativeOnAudioFocusLost();
}

iOS 音频

iOS提供多种音频API,推荐高级API为AVAudioEngine,底层控制用Core Audio。

AVAudioEngine

if (!isPlaying) {
[playerNode play];
isPlaying = true;
}

return true;
}
}

void stop() {
@autoreleasepool {
if (playerNode && isPlaying) {
[playerNode stop];
isPlaying = false;
}
}
}

void shutdown() {
@autoreleasepool {
if (audioEngine) {
[audioEngine stop];
audioEngine = nil;
}

playerNode = nil;
isPlaying = false;
}
}

private:
AVAudioEngine* audioEngine;
AVAudioPlayerNode* playerNode;
bool isPlaying;
};

// 使用:
// iOSAudioEngine audioEngine;
// audioEngine.initialize();
//
// // 创建音频数据
// std::vector<float> audioData = createAudioData();
// audioEngine.playSound(audioData, 44100, 2);
//
// 结束:
// audioEngine.stop();
// audioEngine.shutdown();

Core Audio

// Objective-C++ 代码
#import <AudioToolbox/AudioToolbox.h>
#include <vector>
#include <cmath>

class CoreAudioEngine {
public:
CoreAudioEngine() : audioUnit(nullptr), isInitialized(false) {}

bool initialize() {
// 设置音频组件描述
AudioComponentDescription desc;
desc.componentType = kAudioUnitType_Output;
desc.componentSubType = kAudioUnitSubType_RemoteIO;
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
desc.componentFlags = 0;
desc.componentFlagsMask = 0;

// 查找音频组件
AudioComponent component = AudioComponentFindNext(NULL, &desc);
if (!component) {
NSLog(@"Failed to find audio component");
return false;
}

// 创建音频单元实例
OSStatus status = AudioComponentInstanceNew(component, &audioUnit);
if (status != noErr) {
NSLog(@"Failed to create audio unit: %d", (int)status);
return false;
}

// 启用输出
UInt32 enableOutput = 1;
status = AudioUnitSetProperty(audioUnit,
kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Output,
0,
&enableOutput,
sizeof(enableOutput));
if (status != noErr) {
NSLog(@"Failed to enable audio output: %d", (int)status);
return false;
}

// 设置音频格式
AudioStreamBasicDescription audioFormat;
audioFormat.mSampleRate = 44100;
audioFormat.mFormatID = kAudioFormatLinearPCM;
audioFormat.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked | kAudioFormatFlagIsNonInterleaved;
audioFormat.mBytesPerPacket = 4;
audioFormat.mFramesPerPacket = 1;
audioFormat.mBytesPerFrame = 4;
audioFormat.mChannelsPerFrame = 2;
audioFormat.mBitsPerChannel = 32;

status = AudioUnitSetProperty(audioUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
0,
&audioFormat,
sizeof(audioFormat));
if (status != noErr) {
NSLog(@"Failed to set audio format: %d", (int)status);
return false;
}

// 设置渲染回调
AURenderCallbackStruct callbackStruct;
callbackStruct.inputProc = renderCallback;
callbackStruct.inputProcRefCon = this;

status = AudioUnitSetProperty(audioUnit,
kAudioUnitProperty_SetRenderCallback,
kAudioUnitScope_Input,
0,
&callbackStruct,
sizeof(callbackStruct));
if (status != noErr) {
NSLog(@"Failed to set render callback: %d", (int)status);
return false;
}

// 初始化音频单元
status = AudioUnitInitialize(audioUnit);
if (status != noErr) {
NSLog(@"Failed to initialize audio unit: %d", (int)status);
return false;
}

isInitialized = true;
return true;
}

bool start() {
if (!isInitialized) {
return false;
}

OSStatus status = AudioOutputUnitStart(audioUnit);
if (status != noErr) {
NSLog(@"Failed to start audio unit: %d", (int)status);
return false;
}

return true;
}

void stop() {
if (isInitialized) {
AudioOutputUnitStop(audioUnit);
}
}

void shutdown() {
if (isInitialized) {
stop();
AudioUnitUninitialize(audioUnit);
AudioComponentInstanceDispose(audioUnit);
audioUnit = nullptr;
isInitialized = false;
}
}

private:
AudioUnit audioUnit;
bool isInitialized;
float phase = 0.0f;

// 音频渲染回调
static OSStatus renderCallback(void* inRefCon,
AudioUnitRenderActionFlags* ioActionFlags,
const AudioTimeStamp* inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList* ioData) {
CoreAudioEngine* engine = static_cast<CoreAudioEngine*>(inRefCon);
return engine->render(ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, ioData);
}

OSStatus render(AudioUnitRenderActionFlags* ioActionFlags,
const AudioTimeStamp* inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList* ioData) {
// 生成音频数据,如正弦波
for (UInt32 i = 0; i < ioData->mNumberBuffers; i++) {
float* buffer = static_cast<float*>(ioData->mBuffers[i].mData);

for (UInt32 frame = 0; frame < inNumberFrames; frame++) {
float sample = 0.5f * sinf(phase);
buffer[frame] = sample;

phase += 0.01f;
if (phase > 2.0f * M_PI) {
phase -= 2.0f * M_PI;
}
}
}

return noErr;
}
};

// 使用:
// CoreAudioEngine audioEngine;
// audioEngine.initialize();
// audioEngine.start();
//
// 结束:
// audioEngine.stop();
// audioEngine.shutdown();

iOS 音频会话

管理音频会话对iOS音频行为至关重要:

// This is Objective-C++ code that would be used in your iOS application
#import <AVFoundation/AVFoundation.h>

class AudioSessionManager {
public:
bool configureAudioSession() {
@autoreleasepool {
NSError* error = nil;

// Get the shared audio session
AVAudioSession* session = [AVAudioSession sharedInstance];

// Set the category
if (![session setCategory:AVAudioSessionCategoryAmbient
withOptions:0
error:&error]) {
NSLog(@"Failed to set audio session category: %@", error);
return false;
}

// Set the mode
if (![session setMode:AVAudioSessionModeDefault error:&error]) {
NSLog(@"Failed to set audio session mode: %@", error);
return false;
}

// Activate the audio session
if (![session setActive:YES error:&error]) {
NSLog(@"Failed to activate audio session: %@", error);
return false;
}

// Register for interruptions
[[NSNotificationCenter defaultCenter] addObserver:[NSObject new]
selector:@selector(handleInterruption:)
name:AVAudioSessionInterruptionNotification
object:nil];

return true;
}
}

void handleInterruption(NSNotification* notification) {
@autoreleasepool {
NSDictionary* info = notification.userInfo;
NSInteger type = [[info valueForKey:AVAudioSessionInterruptionTypeKey] integerValue];

if (type == AVAudioSessionInterruptionTypeBegan) {
// Audio session interrupted – pause audio
NSLog(@"Audio session interrupted");
// audioEngine->stop();
} else if (type == AVAudioSessionInterruptionTypeEnded) {
NSInteger options = [[info valueForKey:AVAudioSessionInterruptionOptionKey] integerValue];
if (options == AVAudioSessionInterruptionOptionShouldResume) {
// Interruption ended – resume audio
NSLog(@"Audio session interruption ended");
// audioEngine->start();
}
}
}
}
};

// Usage:
// AudioSessionManager sessionManager;
// sessionManager.configureAudioSession();

移动平台音频注意事项

在为移动平台开发音频功能时,需考虑以下几点:

电量消耗

音频处理可能占用大量 CPU 并消耗电量,可采用以下策略:

  • 降低采样率:在不需要高保真度时使用更低的采样率。
  • 更大块处理:使用更大的 buffer 尺寸处理音频,减少 CPU 唤醒次数。
  • 暂停音频:当应用进入后台或不需要音频时,暂停音频处理。

内存管理

移动设备内存有限:

  • 流式音频:对大型音频文件采用流式加载,而非一次性全部载入内存。
  • 卸载未使用资源:在不需要时卸载音频资源。
  • 压缩音频:使用适合移动平台的压缩格式(iOS 使用 AAC,Android 使用 Opus)。

中断与音频焦点

优雅地处理音频中断:

  • 保存状态:发生中断时保存音频状态,以便后续恢复。
  • 使用系统音量:使用系统音量控制,而非自行实现。
  • 处理通话:在通话或其他系统中断期间暂停音频。

延迟

不同设备的音频延迟特性不同:

  • 在真实设备上测试:模拟器的音频行为可能与真机不同。
  • 使用低延迟模式:Android 和 iOS 都为实时应用提供低延迟音频模式。
  • 合理设置缓冲:通过合适的缓冲大小平衡延迟与音频稳定性。

与 Vulkan 集成

将这些窗口与输入库和 Vulkan 一起使用时,有几个关键注意事项:

表面创建

每个窗口库都提供创建 VkSurfaceKHR 对象的方法,它是 Vulkan 与窗口系统之间的桥梁:

  • GLFW:glfwCreateWindowSurface
  • SDL2:SDL_Vulkan_CreateSurface
  • Win32:vkCreateWin32SurfaceKHR
  • XCB:vkCreateXcbSurfaceKHR
  • Wayland:vkCreateWaylandSurfaceKHR
  • macOS:vkCreateMacOSSurfaceMVK
  • iOS:vkCreateIOSSurfaceMVK
  • Android:vkCreateAndroidSurfaceKHR
  • Metal:vkCreateMetalSurfaceEXT

交换链管理

窗口大小改变时需要重新创建 swapchain,基本流程如下:

void handleWindowResize(VkDevice device, VkSwapchainKHR& swapchain, VkSurfaceKHR surface) {
// 等待设备空闲
vkDeviceWaitIdle(device);

// 销毁旧交换链
VkSwapchainKHR oldSwapchain = swapchain;

// 获取新的表面能力
VkSurfaceCapabilitiesKHR capabilities;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, &capabilities);

// 创建新交换链
VkSwapchainCreateInfoKHR createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
createInfo.surface = surface;
createInfo.minImageCount = capabilities.minImageCount + 1;
createInfo.imageFormat = surfaceFormat.format;
createInfo.imageColorSpace = surfaceFormat.colorSpace;
createInfo.imageExtent = capabilities.currentExtent;
createInfo.imageArrayLayers = 1;
createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
createInfo.preTransform = capabilities.currentTransform;
createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
createInfo.presentMode = presentMode;
createInfo.clipped = VK_TRUE;
createInfo.oldSwapchain = oldSwapchain;

VkResult result = vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapchain);
if (result != VK_SUCCESS) {
throw std::runtime_error("Failed to create swapchain");
}

// 销毁旧交换链
if (oldSwapchain != VK_NULL_HANDLE) {
vkDestroySwapchainKHR(device, oldSwapchain, nullptr);
}

// 重新创建交换链图像、视图、帧缓冲等
// …
}

输入到 Vulkan 渲染

输入处理通常会改变应用状态,进而影响 Vulkan 渲染:

struct AppState {
float cameraPosition[3] = {0.0f, 0.0f, 0.0f};
float cameraRotation[3] = {0.0f, 0.0f, 0.0f};
// 其他状态变量
};

// 根据输入更新状态
void handleInput(AppState& state, float deltaTime) {
// GLFW 示例
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) {
state.cameraPosition[2] -= 1.0f * deltaTime;
}
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) {
state.cameraPosition[2] += 1.0f * deltaTime;
}
// 处理其他按键与输入
}

// 主循环
AppState state;
float lastFrameTime = 0.0f;

while (!glfwWindowShouldClose(window)) {
float currentTime = glfwGetTime();
float deltaTime = currentTime – lastFrameTime;
lastFrameTime = currentTime;

glfwPollEvents();
handleInput(state, deltaTime);

// 使用新状态更新 uniform 缓冲
updateUniformBuffers(state);

// 使用 Vulkan 渲染帧
drawFrame();
}


最佳实践

性能注意事项

  • 减少窗口大小变化:重建 swapchain 开销较大,需高效处理窗口大小变化。
  • 批量处理输入:一次性处理所有输入事件,而非逐个处理。
  • 使用双缓冲:音频使用双缓冲,在准备下一段音频时保证流畅播放。

跨平台开发

  • 抽象平台相关代码:创建平台抽象层,处理不同平台之间的差异。
  • 使用跨平台库:GLFW、SDL2 等库已处理大部分平台相关细节。
  • 在所有目标平台测试:不同平台行为可能存在细微差异。

错误处理

始终检查返回值并优雅处理错误:

VkResult result = vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapchain);
if (result != VK_SUCCESS) {
switch (result) {
case VK_ERROR_OUT_OF_HOST_MEMORY:
std::cerr << "Failed to create swapchain: Out of host memory" << std::endl;
break;
case VK_ERROR_OUT_OF_DEVICE_MEMORY:
std::cerr << "Failed to create swapchain: Out of device memory" << std::endl;
break;
case VK_ERROR_DEVICE_LOST:
std::cerr << "Failed to create swapchain: Device lost" << std::endl;
break;
case VK_ERROR_SURFACE_LOST_KHR:
std::cerr << "Failed to create swapchain: Surface lost" << std::endl;
break;
case VK_ERROR_NATIVE_WINDOW_IN_USE_KHR:
std::cerr << "Failed to create swapchain: Native window in use" << std::endl;
break;
default:
std::cerr << "Failed to create swapchain: Unknown error" << std::endl;
break;
}
// 适当处理错误
}


Libraries

  • GLFW

  • SDL2

  • OpenAL

  • FMOD

Examples and Tutorials

  • Khronos Vulkan Samples

  • Sascha Willems' Vulkan Examples

  • Vulkan Tutorial

Documentation

  • GLFW Documentation

  • SDL2 Wiki

  • OpenAL Documentation

  • FMOD Documentation

  • Oboe GitHub Repository

  • Oboe Documentation

  • iOS AVAudioEngine Documentation

  • iOS Core Audio Documentation

赞(0)
未经允许不得转载:171主机测评 » Using Vulkan -- Windowing, Audio, and Input
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址