Vulkan 基础全景解析:从 Pipeline、Descriptor Set 到 Texture 与 glTF 模型加载
一、为什么 Vulkan 很难,但又值得学习
如果你已经接触过 OpenGL、DirectX 11 或 Unity、Unreal 这类成熟引擎,那么第一次学习 Vulkan 时很容易产生一种感觉:
“为什么只是画一个三角形,竟然要写这么多代码?”
这是正常的。
因为 Vulkan 并不是一个“帮你自动管理渲染状态”的图形 API,而是一个“把 GPU 控制权尽可能交还给程序员”的现代底层图形与计算 API。它要求开发者显式管理:
-
GPU 设备与队列;
-
显存分配;
-
Buffer 与 Image;
-
Pipeline 状态;
-
Shader 资源绑定;
-
Command Buffer 录制;
-
GPU 同步;
-
Swapchain 呈现;
-
纹理采样;
-
模型数据上传;
-
多帧并行资源管理。
OpenGL 的典型风格是:
glBindTexture(...);
glUseProgram(...);
glDrawArrays(...);
很多状态是隐式存在于全局上下文中的。
而 Vulkan 的典型风格是:
vkCmdBindPipeline(...);
vkCmdBindDescriptorSets(...);
vkCmdBindVertexBuffers(...);
vkCmdDrawIndexed(...);
你必须明确告诉 GPU:
现在使用哪个 pipeline?
shader 能访问哪些资源?
顶点数据在哪里?
索引数据在哪里?
纹理在哪里?
渲染目标是什么?
什么时候开始?什么时候结束?
读写资源之间有没有同步关系?
因此,Vulkan 的学习重点不在于记住某个函数怎么调用,而在于建立一套正确的“GPU 工作流心智模型”。
二、Vulkan 的核心思想:显式、预编译、批量提交
可以把 Vulkan 理解为三个核心思想。
1. 显式控制
Vulkan 不喜欢“自动”。
它希望应用程序自己说明资源如何创建、如何使用、何时同步、何时销毁。
这带来的代价是代码量增加。
但收益是:
-
减少驱动层猜测;
-
降低运行时状态检查;
-
提高多线程提交能力;
-
更容易做现代渲染引擎的资源调度;
-
更贴近 GPU 的真实执行模型。
2. 预创建状态对象
在 OpenGL 中,很多渲染状态可以随时修改。
例如当前绑定的 shader、混合状态、深度测试状态、光栅化状态等。
而 Vulkan 倾向于把这些状态提前组合成对象,例如:
-
VkPipeline -
VkPipelineLayout -
VkDescriptorSetLayout -
VkRenderPass -
VkFramebuffer -
VkSampler
尤其是 graphics pipeline,创建成本较高,但使用时非常高效。
这符合现代引擎的思想:
运行前或加载阶段尽量准备好状态对象,运行时只做绑定和提交。
3. Command Buffer 批量录制
Vulkan 不直接要求你每次 draw call 都立即执行。
通常流程是:
开始录制 Command Buffer
绑定 Pipeline
绑定 Descriptor Set
绑定 Vertex Buffer
绑定 Index Buffer
发出 Draw Call
结束录制 Command Buffer
提交到 Queue
GPU 执行
Command Buffer 是 Vulkan 的核心执行容器。
CPU 将命令录制进去,然后提交给 GPU 队列执行。
这使得 Vulkan 很适合多线程渲染:
不同线程可以并行录制不同 command buffer,然后由主线程统一提交。
三、Vulkan 程序的基本结构
一个最小的 Vulkan 渲染程序,大致包含以下模块:
Application
├── Instance
├── Surface
├── Physical Device
├── Logical Device
├── Queue
├── Swapchain
├── Image Views
├── Render Pass / Dynamic Rendering
├── Framebuffers
├── Descriptor Set Layout
├── Pipeline Layout
├── Graphics Pipeline
├── Command Pool
├── Command Buffers
├── Vertex Buffer / Index Buffer
├── Uniform Buffer
├── Texture Image / Image View / Sampler
├── Descriptor Pool
├── Descriptor Sets
├── Synchronization Objects
└── Render Loop
初学者往往会被这些概念淹没。
但从渲染过程看,它们并不混乱。
你可以把它们分为五类。
第一类:上下文与设备
包括:
-
VkInstance -
VkPhysicalDevice -
VkDevice -
VkQueue
它们回答的问题是:
我使用哪个 Vulkan 环境?
我选择哪块 GPU?
我创建哪个逻辑设备?
我往哪个队列提交任务?
第二类:窗口显示系统
包括:
-
VkSurfaceKHR -
VkSwapchainKHR -
swapchain images
-
image views
它们回答的问题是:
我要把最终图像显示到哪个窗口?
屏幕缓冲区有几张图像?
当前应该渲染到哪一张?
第三类:资源系统
包括:
-
VkBuffer -
VkImage -
VkDeviceMemory -
VkImageView -
VkSampler
它们回答的问题是:
顶点、索引、矩阵、材质、纹理、深度图、颜色图存在哪里?
这些数据如何被 GPU 访问?
第四类:Pipeline 与 Shader 绑定系统
包括:
-
VkShaderModule -
VkPipeline -
VkPipelineLayout -
VkDescriptorSetLayout -
VkDescriptorSet -
push constants
它们回答的问题是:
GPU 应该如何处理顶点?
如何光栅化?
如何执行片元着色?
shader 能访问哪些资源?
第五类:命令与同步系统
包括:
-
VkCommandPool -
VkCommandBuffer -
VkSemaphore -
VkFence -
pipeline barrier
它们回答的问题是:
CPU 如何把命令发给 GPU?
GPU 任务之间如何排序?
当前帧何时可以写?
渲染结束后何时可以 present?
四、Instance、Physical Device 与 Logical Device
1. VkInstance:Vulkan 程序入口
VkInstance 是 Vulkan 应用的全局入口。
它不代表 GPU,而是代表应用程序与 Vulkan loader、validation layer、extension 系统之间的连接。
创建 instance 时通常需要指定:
-
应用名称;
-
Vulkan API 版本;
-
需要启用的 validation layers;
-
需要启用的 instance extensions。
示意代码:
VkApplicationInfo appInfo{};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pApplicationName = "Vulkan Renderer";
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.pEngineName = "Custom Engine";
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.apiVersion = VK_API_VERSION_1_3;
VkInstanceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
vkCreateInstance(&createInfo, nullptr, &instance);
2. Physical Device:真实 GPU
VkPhysicalDevice 表示系统中的物理 GPU。
例如:
-
NVIDIA RTX 显卡;
-
AMD Radeon 显卡;
-
Intel 核显;
-
移动端 Mali / Adreno GPU。
选择 physical device 时,需要检查:
-
是否支持 graphics queue;
-
是否支持 present queue;
-
是否支持 swapchain extension;
-
是否支持所需 feature;
-
显存类型是否满足需求;
-
MSAA、anisotropy、sampler、descriptor 数量等硬件限制。
3. Logical Device:应用与 GPU 的连接
VkDevice 是逻辑设备。
应用程序并不是直接操作 physical device,而是基于 physical device 创建 logical device。
创建 logical device 时会指定:
-
使用哪些 queue family;
-
创建多少个 queue;
-
启用哪些 device extensions;
-
启用哪些 features。
例如要使用纹理各向异性过滤,就需要检查并启用:
VkPhysicalDeviceFeatures deviceFeatures{};
deviceFeatures.samplerAnisotropy = VK_TRUE;
五、Queue 与 Queue Family
GPU 内部可能支持多种任务队列:
-
graphics queue:执行图形渲染;
-
compute queue:执行计算任务;
-
transfer queue:执行数据拷贝;
-
present queue:负责把 swapchain image 显示到窗口。
Vulkan 使用 queue family 描述这些能力。
一个 queue family 可以支持一种或多种能力。
典型渲染程序至少需要:
Graphics Queue
Present Queue
有些 GPU 上 graphics queue 和 present queue 是同一个 queue family。
有些平台则可能分开。
这就是为什么 Vulkan 初始化阶段需要查找 queue family。
六、Swapchain:显示到屏幕的图像队列
1. 什么是 Swapchain
Swapchain 是一组可以被渲染并最终显示到屏幕的图像。
简单理解:
CPU / GPU 渲染一张图像
↓
图像进入 swapchain
↓
显示系统把图像 present 到窗口
通常 swapchain 包含多张 image:
-
双缓冲:2 张;
-
三缓冲:3 张。
多缓冲的目的是让 CPU、GPU、显示系统可以并行工作,减少等待。
2. Swapchain 需要确定什么
创建 swapchain 时,需要确定:
-
图像格式,例如
VK_FORMAT_B8G8R8A8_SRGB; -
色彩空间;
-
分辨率 extent;
-
present mode;
-
image count;
-
image usage;
-
transform;
-
composite alpha。
3. Present Mode
常见 present mode:
VK_PRESENT_MODE_FIFO_KHR
类似垂直同步,基本所有平台都支持。
VK_PRESENT_MODE_MAILBOX_KHR
类似三缓冲低延迟模式,如果平台支持,常用于实时渲染。
七、Image View 与 Framebuffer
Swapchain 给你的是 VkImage。
但是 shader、render pass 或 framebuffer 通常不能直接使用裸 VkImage,而是要通过 VkImageView 访问。
VkImageView 可以理解为:
对 VkImage 的一种视图解释方式
它决定:
-
使用哪种格式解释 image;
-
使用哪个 mip level;
-
使用哪个 array layer;
-
用作 color、depth 还是 stencil。
Framebuffer 则把具体的 image view 绑定到 render pass 的 attachment 上。
例如:
Render Pass 声明:我需要一个 color attachment 和一个 depth attachment
Framebuffer 提供:这一次渲染具体用 swapchain image view A 和 depth image view B
八、Render Pass 与 Dynamic Rendering
1. Render Pass 的传统模型
传统 Vulkan 中,VkRenderPass 描述一次渲染过程中的 attachment 使用方式。
它会说明:
-
有哪些 color attachment;
-
是否有 depth/stencil attachment;
-
attachment 初始 layout;
-
attachment 最终 layout;
-
load 操作;
-
store 操作;
-
subpass 结构;
-
subpass dependency。
例如:
开始时清空 color attachment
渲染过程中作为 color output 使用
结束后转换为 present layout
2. Dynamic Rendering 的现代趋势
较新的 Vulkan 版本和扩展引入了 dynamic rendering。
它减少了传统 render pass / framebuffer 的固定绑定关系,让应用可以在命令录制时直接指定渲染目标。
传统方式:
RenderPass + Framebuffer + Subpass
Dynamic Rendering 方式:
vkCmdBeginRendering
vkCmdEndRendering
对于现代引擎来说,dynamic rendering 更灵活,尤其适合:
-
多 pass 渲染;
-
后处理;
-
延迟渲染;
-
动态渲染目标切换;
-
frame graph 系统。
但初学者仍然建议先理解传统 render pass,因为它能帮助你掌握 attachment、layout、subpass、load/store 等核心概念。
九、Graphics Pipeline:Vulkan 最核心的状态对象
1. Pipeline 是什么
Graphics pipeline 是 Vulkan 中最重要的对象之一。
它描述 GPU 如何从顶点数据生成最终像素。
一个典型 graphics pipeline 包含:
Vertex Input
Input Assembly
Vertex Shader
Tessellation Shader 可选
Geometry Shader 可选
Viewport / Scissor
Rasterization
Multisampling
Depth / Stencil Test
Fragment Shader
Color Blend
Pipeline Layout
Render Pass / Rendering Info
可以把 pipeline 理解为:
GPU 渲染状态的完整快照
在 Vulkan 中,很多状态不是 draw call 时随便改,而是提前固定进 pipeline。
这使得驱动可以提前编译和优化。
2. Shader Stage
最常见的 graphics shader stage 是:
Vertex Shader
Fragment Shader
Vertex Shader 负责处理顶点。
它通常完成:
-
模型空间到世界空间变换;
-
世界空间到观察空间变换;
-
观察空间到裁剪空间变换;
-
输出纹理坐标;
-
输出法线;
-
输出颜色;
-
输出 tangent basis。
Fragment Shader 负责处理像素片元。
它通常完成:
-
纹理采样;
-
光照计算;
-
PBR 材质计算;
-
alpha test;
-
normal mapping;
-
输出最终颜色。
3. Vertex Input
Vertex Input 描述顶点缓冲区的内存结构。
例如一个顶点结构:
struct Vertex {
glm::vec3 position;
glm::vec3 normal;
glm::vec2 texCoord;
};
在 Vulkan 中,需要告诉 pipeline:
-
每个顶点多大;
-
position 在结构体中的 offset;
-
normal 在结构体中的 offset;
-
texCoord 在结构体中的 offset;
-
每个 attribute 的格式是什么。
示意:
VkVertexInputBindingDescription binding{};
binding.binding = 0;
binding.stride = sizeof(Vertex);
binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
std::array<VkVertexInputAttributeDescription, 3> attributes{};
attributes[0].binding = 0;
attributes[0].location = 0;
attributes[0].format = VK_FORMAT_R32G32B32_SFLOAT;
attributes[0].offset = offsetof(Vertex, position);
attributes[1].binding = 0;
attributes[1].location = 1;
attributes[1].format = VK_FORMAT_R32G32B32_SFLOAT;
attributes[1].offset = offsetof(Vertex, normal);
attributes[2].binding = 0;
attributes[2].location = 2;
attributes[2].format = VK_FORMAT_R32G32_SFLOAT;
attributes[2].offset = offsetof(Vertex, texCoord);
这里的 location 必须与 shader 中的输入匹配:
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inNormal;
layout(location = 2) in vec2 inTexCoord;
4. Input Assembly
Input Assembly 决定顶点如何组成图元。
常见类型:
VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST
VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP
VK_PRIMITIVE_TOPOLOGY_LINE_LIST
VK_PRIMITIVE_TOPOLOGY_POINT_LIST
最常见的是 triangle list。
例如每 3 个顶点组成一个三角形。
5. Viewport 与 Scissor
Viewport 决定 NDC 坐标如何映射到屏幕区域。
Scissor 决定实际允许写入的矩形区域。
Viewport 是坐标变换问题。
Scissor 是裁剪区域问题。
6. Rasterization
Rasterization 是把三角形转换为片元的过程。
相关状态包括:
-
polygon mode;
-
cull mode;
-
front face;
-
depth bias;
-
line width。
例如背面剔除:
rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
7. Depth / Stencil
深度测试用于解决遮挡关系。
如果没有深度测试,后画的物体可能覆盖先画的物体,即使它在空间上更远。
常见配置:
depthStencil.depthTestEnable = VK_TRUE;
depthStencil.depthWriteEnable = VK_TRUE;
depthStencil.depthCompareOp = VK_COMPARE_OP_LESS;
含义是:
如果新片元深度值更小,说明它离相机更近,允许通过测试并写入深度缓冲。
8. Color Blend
Color Blend 决定 fragment shader 输出颜色如何与 framebuffer 中已有颜色混合。
不透明物体通常关闭 blending:
blendAttachment.blendEnable = VK_FALSE;
透明物体通常开启 alpha blending:
blendAttachment.blendEnable = VK_TRUE;
blendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
blendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
blendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
9. Pipeline Layout
Pipeline Layout 是 shader 资源接口的总描述。
它包含:
-
descriptor set layouts;
-
push constant ranges。
也就是说,pipeline layout 决定了 shader 可以访问哪些外部资源。
例如:
set = 0:全局相机 UBO
set = 1:材质纹理
push constants:当前物体的 model matrix 或 object id
十、Descriptor Set:Shader 访问资源的桥梁
1. Descriptor 是什么
Shader 不能凭空访问 CPU 侧对象。
它需要通过 descriptor 访问 GPU 资源。
Descriptor 可以描述:
-
uniform buffer;
-
storage buffer;
-
sampled image;
-
sampler;
-
combined image sampler;
-
storage image;
-
acceleration structure。
Descriptor Set 是一组 descriptor 的集合。
Descriptor Set Layout 则描述这一组 descriptor 的结构。
可以这样理解:
DescriptorSetLayout = 资源接口声明
DescriptorSet = 具体绑定的资源实例
2. Descriptor Set 的三步使用
使用 descriptor set 通常分三步。
第一步:创建 descriptor set layout。
VkDescriptorSetLayoutBinding uboBinding{};
uboBinding.binding = 0;
uboBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
uboBinding.descriptorCount = 1;
uboBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
第二步:从 descriptor pool 分配 descriptor set。
vkAllocateDescriptorSets(device, &allocInfo, &descriptorSet);
第三步:更新 descriptor set,让它指向具体 buffer 或 image。
VkDescriptorBufferInfo bufferInfo{};
bufferInfo.buffer = uniformBuffer;
bufferInfo.offset = 0;
bufferInfo.range = sizeof(UniformBufferObject);
VkWriteDescriptorSet descriptorWrite{};
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrite.dstSet = descriptorSet;
descriptorWrite.dstBinding = 0;
descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorWrite.descriptorCount = 1;
descriptorWrite.pBufferInfo = &bufferInfo;
vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);
渲染时绑定:
vkCmdBindDescriptorSets(
commandBuffer,
VK_PIPELINE_BIND_POINT_GRAPHICS,
pipelineLayout,
0,
1,
&descriptorSet,
0,
nullptr
);
3. Descriptor Set 的分组策略
实际引擎中,不应该把所有资源塞进一个 descriptor set。
更合理的做法是按更新频率分组。
例如:
set = 0:Frame 级资源
Camera UBO
Light UBO
Shadow Map
Environment Map
set = 1:Material 级资源
BaseColor Texture
Normal Texture
MetallicRoughness Texture
Material Parameters
set = 2:Object 级资源
Object Transform
Object ID
Skinning Matrices
为什么按更新频率分组?
因为相机每帧更新,材质不一定每个物体都变,object transform 每个物体可能都变。
合理分组可以减少 descriptor 绑定次数,提高渲染效率。
十一、Uniform Buffer:传递矩阵与全局参数
Uniform Buffer 常用于存放小规模、只读、频繁访问的 shader 参数。
典型 UBO:
struct UniformBufferObject {
glm::mat4 model;
glm::mat4 view;
glm::mat4 proj;
};
Vertex Shader 中:
layout(set = 0, binding = 0) uniform UBO {
mat4 model;
mat4 view;
mat4 proj;
} ubo;
layout(location = 0) in vec3 inPosition;
void main() {
gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0);
}
这就是最经典的 MVP 变换:
Model Matrix : 模型空间 → 世界空间
View Matrix : 世界空间 → 相机空间
Projection Matrix : 相机空间 → 裁剪空间
需要注意,Vulkan 的裁剪空间和 OpenGL 有差异。
常见处理方式是:
ubo.proj[1][1] *= -1;
这是因为 Vulkan 的屏幕坐标约定与 OpenGL 不完全一致。
十二、Dynamic Uniform Buffer:多个物体共享一个大 UBO
1. 为什么需要 Dynamic Uniform Buffer
如果场景中有 1000 个物体,每个物体都有自己的 model matrix,最直接的做法是:
每个物体一个 uniform buffer
每个物体一个 descriptor set
这当然可行,但管理成本较高。
Dynamic Uniform Buffer 提供另一种方式:
创建一个大的 uniform buffer
里面连续存储多个物体的数据
渲染每个物体时通过 dynamic offset 指向不同位置
示意:
Dynamic UBO
├── Object 0 matrix
├── Object 1 matrix
├── Object 2 matrix
├── Object 3 matrix
└── ...
绘制第 0 个物体:
dynamicOffset = 0;
vkCmdBindDescriptorSets(..., 1, &dynamicOffset);
绘制第 1 个物体:
dynamicOffset = alignedSize * 1;
vkCmdBindDescriptorSets(..., 1, &dynamicOffset);
绘制第 N 个物体:
dynamicOffset = alignedSize * N;
vkCmdBindDescriptorSets(..., 1, &dynamicOffset);
2. Dynamic UBO 的对齐问题
这是初学者最容易出错的地方。
Vulkan 对 dynamic uniform buffer offset 有对齐要求。
这个要求来自:
VkPhysicalDeviceLimits::minUniformBufferOffsetAlignment
因此不能简单使用:
sizeof(ObjectData)
作为步长。
应该使用对齐后的大小:
VkDeviceSize alignTo(VkDeviceSize value, VkDeviceSize alignment) {
return (value + alignment - 1) & ~(alignment - 1);
}
VkDeviceSize objectSize = sizeof(ObjectData);
VkDeviceSize alignedSize = alignTo(objectSize, minUniformBufferOffsetAlignment);
否则 shader 读取数据可能错位,甚至 validation layer 报错。
3. Dynamic UBO 的适用场景
适合:
-
每个物体一个 model matrix;
-
每个物体少量参数;
-
数据量不大;
-
更新频率高;
-
draw call 中通过 dynamic offset 快速切换。
不适合:
-
大规模实例数据;
-
大数组;
-
GPU 写入数据;
-
复杂材质表;
-
海量骨骼矩阵。
这些情况更适合 storage buffer 或 structured buffer。
十三、Push Constants:小数据的高速传递方式
1. Push Constants 是什么
Push Constants 是 Vulkan 中用于向 shader 传递少量数据的机制。
它不需要创建 buffer,也不需要更新 descriptor set。
典型用途:
-
当前物体 ID;
-
当前材质 ID;
-
一个小矩阵;
-
draw call 级别的开关;
-
少量渲染参数。
示例结构:
struct PushConstants {
glm::mat4 model;
int materialIndex;
};
Pipeline Layout 中声明 push constant range:
VkPushConstantRange pushRange{};
pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
pushRange.offset = 0;
pushRange.size = sizeof(PushConstants);
录制 command buffer 时写入:
vkCmdPushConstants(
commandBuffer,
pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
0,
sizeof(PushConstants),
&pushData
);
Shader 中读取:
layout(push_constant) uniform PushConstants {
mat4 model;
int materialIndex;
} pc;
2. Push Constants 与 UBO 的区别
| 机制 | 适合数据 | 是否需要 Descriptor | 更新频率 |
|---|---|---|---|
| Uniform Buffer | 相机矩阵、光照参数、全局参数 | 需要 | 每帧 / 每对象 |
| Dynamic Uniform Buffer | 多对象小数据 | 需要 | 每 draw 切换 offset |
| Push Constants | 极小且频繁变化的数据 | 不需要 | 每 draw 很方便 |
| Storage Buffer | 大规模结构化数据 | 需要 | 灵活 |
3. 使用建议
Push Constants 很方便,但不能滥用。
它的容量通常较小,跨平台保守使用时应存放几十到一百多字节级别的数据。
推荐策略:
Camera / Light → Uniform Buffer
Object Transform → Dynamic UBO 或 Push Constants
Material Parameters → UBO / SSBO / Descriptor
Texture → Combined Image Sampler
Large Object Data → Storage Buffer
十四、Buffer:顶点、索引、Uniform 与 Storage
Vulkan 中 VkBuffer 是线性内存资源。
常见用途:
Vertex Buffer
Index Buffer
Uniform Buffer
Storage Buffer
Staging Buffer
Indirect Draw Buffer
1. Vertex Buffer
存储顶点数据:
struct Vertex {
glm::vec3 position;
glm::vec3 normal;
glm::vec2 texCoord;
};
绘制时绑定:
VkBuffer vertexBuffers[] = { vertexBuffer };
VkDeviceSize offsets[] = { 0 };
vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets);
2. Index Buffer
索引缓冲区用于复用顶点。
vkCmdBindIndexBuffer(commandBuffer, indexBuffer, 0, VK_INDEX_TYPE_UINT32);
vkCmdDrawIndexed(commandBuffer, indexCount, 1, 0, 0, 0);
相比直接 vkCmdDraw,indexed drawing 能减少重复顶点数据。
3. Staging Buffer
很多 GPU 本地显存不能直接被 CPU 高效写入。
因此常见做法是:
CPU 写入 staging buffer
↓
vkCmdCopyBuffer
↓
拷贝到 device local vertex buffer
Staging buffer 通常使用:
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
最终 GPU 使用的 buffer 通常使用:
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
4. Storage Buffer
Storage Buffer 比 Uniform Buffer 更灵活。
它可以存储大量结构化数据,也可以被 compute shader 写入。
适合:
-
粒子系统;
-
GPU culling;
-
骨骼矩阵数组;
-
材质数组;
-
实例数据;
-
大规模场景对象数据;
-
clustered lighting 数据。
十五、Vulkan 内存模型:Buffer 和 Memory 是分离的
这是 Vulkan 与很多高级 API 不同的地方。
创建一个 buffer:
vkCreateBuffer(device, &bufferInfo, nullptr, &buffer);
这一步只是创建 buffer 对象。
它还没有真正绑定显存。
接着需要查询内存需求:
vkGetBufferMemoryRequirements(device, buffer, &memRequirements);
然后分配 memory:
vkAllocateMemory(device, &allocInfo, nullptr, &bufferMemory);
最后绑定:
vkBindBufferMemory(device, buffer, bufferMemory, 0);
也就是说:
VkBuffer = 资源对象
VkDeviceMemory = 实际显存
实际工程中,不建议每个 buffer 都单独 vkAllocateMemory。
更好的方式是使用 Vulkan Memory Allocator,也就是 VMA。
VMA 可以帮助你做显存分配、子分配、映射、释放和统计。
十六、Texture:Image、ImageView、Sampler 与 Layout
1. Texture 在 Vulkan 中由什么组成
Vulkan 中一张可采样纹理通常由三部分组成:
VkImage : 图像存储本体
VkImageView : 图像视图
VkSampler : 采样器
VkImage 负责存储像素数据。VkImageView 负责告诉 shader 如何解释这张 image。VkSampler 负责描述采样方式。
例如:
线性过滤还是最近点过滤?
是否开启 mipmap?
UV 超出 0~1 后 repeat 还是 clamp?
是否开启各向异性过滤?
2. Texture 上传流程
纹理上传通常分为以下步骤:
读取图片文件
↓
创建 staging buffer
↓
CPU 把像素数据写入 staging buffer
↓
创建 VkImage
↓
转换 image layout:undefined → transfer dst
↓
vkCmdCopyBufferToImage
↓
生成 mipmap 可选
↓
转换 image layout:transfer dst → shader read only
↓
创建 image view
↓
创建 sampler
↓
写入 descriptor set
典型 image layout 转换:
VK_IMAGE_LAYOUT_UNDEFINED
↓
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
↓
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
3. Combined Image Sampler
Fragment Shader 采样纹理时,常用 descriptor 类型是:
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER
Descriptor 更新示意:
VkDescriptorImageInfo imageInfo{};
imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
imageInfo.imageView = textureImageView;
imageInfo.sampler = textureSampler;
VkWriteDescriptorSet descriptorWrite{};
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrite.dstSet = descriptorSet;
descriptorWrite.dstBinding = 1;
descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
descriptorWrite.descriptorCount = 1;
descriptorWrite.pImageInfo = &imageInfo;
vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);
Shader 中:
layout(set = 1, binding = 0) uniform sampler2D baseColorTexture;
layout(location = 0) in vec2 fragTexCoord;
layout(location = 0) out vec4 outColor;
void main() {
outColor = texture(baseColorTexture, fragTexCoord);
}
4. Mipmap
Mipmap 是一组逐级缩小的纹理。
它可以减少远处纹理闪烁,提高采样质量。
例如:
1024 x 1024
512 x 512
256 x 256
128 x 128
...
1 x 1
在 Vulkan 中生成 mipmap 通常需要:
-
image 支持 transfer src;
-
image 支持 transfer dst;
-
格式支持 linear blit;
-
多次 layout transition;
-
多次
vkCmdBlitImage。
5. sRGB 与 Linear
颜色纹理通常使用 sRGB 格式,例如:
VK_FORMAT_R8G8B8A8_SRGB
法线贴图、金属度粗糙度贴图、遮蔽贴图通常不应该使用 sRGB,而应该使用线性格式,例如:
VK_FORMAT_R8G8B8A8_UNORM
原因是:
BaseColor 是颜色,需要 gamma 处理。
Normal / Roughness / Metallic 是数据,不应该被 gamma 校正。
这是 PBR 渲染中非常重要的细节。
十七、同步:Vulkan 最容易出错的部分
Vulkan 的同步是显式的。
如果你没有正确同步,可能出现:
-
GPU 还没写完,下一步就开始读;
-
swapchain image 还没准备好就开始渲染;
-
渲染还没结束就 present;
-
CPU 修改了 GPU 仍在使用的 uniform buffer;
-
image layout 不匹配;
-
随机闪烁;
-
validation layer 报错;
-
某些显卡正常,某些显卡崩溃。
1. Semaphore
Semaphore 用于 GPU 队列之间或队列操作之间的同步。
典型帧流程:
vkAcquireNextImageKHR
signal imageAvailableSemaphore
vkQueueSubmit
wait imageAvailableSemaphore
signal renderFinishedSemaphore
vkQueuePresentKHR
wait renderFinishedSemaphore
也就是说:
等 swapchain image 可用
↓
开始渲染
↓
渲染结束
↓
present 到屏幕
2. Fence
Fence 用于 CPU 等待 GPU。
常见用途:
CPU 不要覆盖 GPU 仍在使用的 per-frame 资源
每帧开始时:
vkWaitForFences(device, 1, &inFlightFence, VK_TRUE, UINT64_MAX);
vkResetFences(device, 1, &inFlightFence);
这表示:
等上一轮使用这套 frame resource 的 GPU 工作完成,再复用它。
3. Pipeline Barrier
Pipeline barrier 用于同步资源访问和 layout transition。
例如 texture 上传时:
Transfer Write
↓
Shader Read
需要 barrier 告诉 GPU:
前面的传输写入必须完成;
然后 fragment shader 才能读取;
同时 image layout 要转换到 shader read only。
同步是 Vulkan 中最体现底层性的部分。
初学者不要试图跳过它,因为真实项目中大量 bug 都来自同步错误。
十八、Command Pool 与 Command Buffer
1. Command Pool
Command Pool 用于分配 command buffer。
它通常和 queue family 绑定。
VkCommandPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.queueFamilyIndex = graphicsQueueFamily;
poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
2. Command Buffer
Command Buffer 是命令录制容器。
典型录制流程:
vkBeginCommandBuffer(commandBuffer, &beginInfo);
vkCmdBeginRenderPass(commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);
vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets);
vkCmdBindIndexBuffer(commandBuffer, indexBuffer, 0, VK_INDEX_TYPE_UINT32);
vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
pipelineLayout, 0, 1, &descriptorSet, 0, nullptr);
vkCmdDrawIndexed(commandBuffer, indexCount, 1, 0, 0, 0);
vkCmdEndRenderPass(commandBuffer);
vkEndCommandBuffer(commandBuffer);
这段代码本质上描述了一帧中 GPU 应该做什么。
十九、渲染循环:一帧到底发生了什么
一个标准 Vulkan 渲染循环可以抽象为:
1. 等待当前 frame 的 fence
2. 从 swapchain 获取 image index
3. 重置 fence
4. 重置 command buffer
5. 录制 command buffer
6. 提交 graphics queue
7. present queue 显示
8. 进入下一帧
伪代码:
void drawFrame() {
vkWaitForFences(device, 1, &inFlightFence[currentFrame], VK_TRUE, UINT64_MAX);
uint32_t imageIndex;
vkAcquireNextImageKHR(
device,
swapchain,
UINT64_MAX,
imageAvailableSemaphore[currentFrame],
VK_NULL_HANDLE,
&imageIndex
);
vkResetFences(device, 1, &inFlightFence[currentFrame]);
vkResetCommandBuffer(commandBuffer[currentFrame], 0);
recordCommandBuffer(commandBuffer[currentFrame], imageIndex);
VkSubmitInfo submitInfo{};
// wait imageAvailable
// signal renderFinished
vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFence[currentFrame]);
VkPresentInfoKHR presentInfo{};
// wait renderFinished
vkQueuePresentKHR(presentQueue, &presentInfo);
currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
}
现代 Vulkan 程序通常使用:
const int MAX_FRAMES_IN_FLIGHT = 2;
这样 CPU 可以准备下一帧,而 GPU 正在处理上一帧。
二十、glTF 模型加载:从文件到 Vulkan 渲染
1. glTF 是什么
glTF 可以理解为“3D 模型领域的 JPEG”。
它的目标不是作为建模软件的完整工程格式,而是作为运行时高效加载和传输的 3D 资产格式。
一个 glTF 模型通常包含:
-
scene;
-
node;
-
mesh;
-
primitive;
-
accessor;
-
buffer;
-
bufferView;
-
material;
-
texture;
-
image;
-
sampler;
-
animation;
-
skin。
对于 Vulkan 渲染器来说,重点通常是:
Mesh Geometry
Material
Texture
Node Transform
Animation
Skinning
2. glTF 的层级结构
glTF 的场景结构大致是:
Scene
└── Node
├── Transform
├── Mesh
│ └── Primitive
│ ├── Attributes
│ ├── Indices
│ └── Material
└── Children
一个 node 可以有变换,也可以引用 mesh。
一个 mesh 可以包含多个 primitive。
每个 primitive 通常对应一个材质。
3. glTF Primitive 到 Vulkan Buffer
glTF primitive 常见 attribute:
POSITION
NORMAL
TEXCOORD_0
TANGENT
COLOR_0
JOINTS_0
WEIGHTS_0
加载时需要把这些 attribute 解析成 Vulkan 顶点结构。
例如:
struct Vertex {
glm::vec3 position;
glm::vec3 normal;
glm::vec4 tangent;
glm::vec2 texCoord;
};
然后创建:
Vertex Buffer
Index Buffer
渲染时:
vkCmdBindVertexBuffers(...);
vkCmdBindIndexBuffer(...);
vkCmdDrawIndexed(...);
4. glTF 材质:PBR Metallic-Roughness
glTF 2.0 默认使用 PBR metallic-roughness 材质模型。
典型参数包括:
baseColorFactor
baseColorTexture
metallicFactor
roughnessFactor
metallicRoughnessTexture
normalTexture
occlusionTexture
emissiveTexture
emissiveFactor
alphaMode
alphaCutoff
doubleSided
在 Vulkan 中,这通常对应:
Material Uniform Buffer / Storage Buffer
+
多个 Combined Image Sampler
例如:
set = 1, binding = 0 : baseColorTexture
set = 1, binding = 1 : normalTexture
set = 1, binding = 2 : metallicRoughnessTexture
set = 1, binding = 3 : occlusionTexture
set = 1, binding = 4 : emissiveTexture
set = 1, binding = 5 : material parameter buffer
5. glTF 纹理通道约定
glTF 的 metallic-roughness texture 通常使用通道打包:
G 通道:roughness
B 通道:metallic
Occlusion texture 通常使用:
R 通道:occlusion
Normal texture 是切线空间法线。
如果模型没有 tangent,需要在加载阶段生成 tangent,常见算法是 MikkTSpace。
6. glTF 坐标与矩阵
glTF 使用右手坐标系。
Vulkan 的 clip space 与 OpenGL 不完全一致,因此模型加载后仍然要注意投影矩阵处理。
每个 node 的变换可能来自:
matrix
或者:
translation
rotation
scale
如果是 TRS,需要组合为:
localMatrix = T * R * S
再通过父子层级递归得到:
worldMatrix = parentWorldMatrix * localMatrix
渲染每个 primitive 时,把 worldMatrix 作为 object transform 传入 shader。
7. glTF 加载流程总结
完整流程如下:
读取 glTF / GLB 文件
↓
解析 scene / node / mesh / material / texture
↓
遍历 node 层级,计算 transform
↓
提取 primitive 顶点属性
↓
创建 vertex buffer
↓
创建 index buffer
↓
加载 image,创建 Vulkan texture
↓
创建 material descriptor set
↓
创建 object transform buffer 或 push constants
↓
录制 command buffer
↓
按 primitive 绑定 pipeline、descriptor、buffer 并 draw indexed
二十一、从“画三角形”到“加载模型”的完整架构
一个可扩展的 Vulkan renderer 可以这样组织:
Renderer
├── VulkanContext
│ ├── Instance
│ ├── PhysicalDevice
│ ├── Device
│ ├── Queues
│ └── CommandPools
│
├── SwapchainManager
│ ├── Swapchain
│ ├── ImageViews
│ ├── DepthImage
│ └── Framebuffers
│
├── PipelineManager
│ ├── ShaderModules
│ ├── PipelineLayouts
│ └── GraphicsPipelines
│
├── DescriptorManager
│ ├── DescriptorSetLayouts
│ ├── DescriptorPools
│ └── DescriptorSets
│
├── ResourceManager
│ ├── Buffers
│ ├── Images
│ ├── Samplers
│ └── Memory Allocator
│
├── ModelManager
│ ├── glTF Loader
│ ├── Meshes
│ ├── Materials
│ └── Textures
│
├── FrameResources
│ ├── CommandBuffer
│ ├── UniformBuffers
│ ├── Semaphores
│ └── Fences
│
└── RenderLoop
初学 Vulkan 最忌讳把所有内容写在一个巨大 main.cpp 中。
更好的方法是从一开始就做模块化拆分。
二十二、Vulkan 中常见资源绑定方案
方案一:简单教学方案
适合画三角形、立方体、小模型。
set = 0:
binding 0: Uniform Buffer, MVP matrix
binding 1: Combined Image Sampler
优点:简单。
缺点:扩展性差。
方案二:按更新频率分组
适合中小型渲染器。
set = 0: Frame Data
Camera
Lights
set = 1: Material Data
Textures
Material parameters
set = 2: Object Data
Transform
优点:结构清晰。
缺点:descriptor 管理稍复杂。
方案三:Bindless / Descriptor Indexing
适合大型引擎。
set = 0:
Global UBO
Big texture array
Big material buffer
Big object buffer
shader 通过 index 访问资源:
texture(textures[material.baseColorTextureIndex], uv);
优点:减少频繁 descriptor 绑定,适合海量材质和对象。
缺点:需要更复杂的 feature 检查和资源管理。
二十三、Vulkan Shader:GLSL、HLSL 与 SPIR-V
Vulkan 不直接消费 GLSL 或 HLSL 源码。
它使用中间表示:
SPIR-V
常见编译路径:
GLSL → glslangValidator → SPIR-V
HLSL → DXC → SPIR-V
Slang → Slang Compiler → SPIR-V
例如 GLSL 编译:
glslangValidator -V shader.vert -o vert.spv
glslangValidator -V shader.frag -o frag.spv
Vulkan 程序加载 SPIR-V 后创建 shader module:
VkShaderModuleCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
createInfo.codeSize = code.size();
createInfo.pCode = reinterpret_cast<const uint32_t*>(code.data());
vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule);
然后在 pipeline creation 中指定 shader stage。
二十四、Validation Layer:Vulkan 开发必开
Vulkan 很底层,错误也很底层。
如果不使用 validation layer,很多 bug 只会表现为:
黑屏
闪烁
崩溃
某些显卡正常,某些显卡异常
开发阶段应该启用:
VK_LAYER_KHRONOS_validation
它可以检查:
-
descriptor 是否绑定正确;
-
image layout 是否匹配;
-
buffer usage 是否正确;
-
同步是否存在明显问题;
-
pipeline state 是否兼容;
-
object 是否提前销毁;
-
command buffer 状态是否正确。
在 Vulkan 学习阶段,validation layer 的报错信息比教程更重要。
遇到错误不要绕过,应该逐条理解。
二十五、Vulkan 常见错误与排查思路
1. 黑屏
可能原因:
-
shader 没有正确编译;
-
pipeline 使用了错误 render pass;
-
viewport / scissor 设置错误;
-
vertex input layout 与 shader location 不匹配;
-
没有绑定 descriptor set;
-
uniform buffer 数据错误;
-
depth test 把物体全部裁掉;
-
front face / cull mode 设置错误。
2. 纹理显示异常
可能原因:
-
image layout 不正确;
-
descriptor imageLayout 设置错误;
-
sampler 没有创建;
-
image view format 不正确;
-
sRGB / UNORM 使用错误;
-
staging buffer 拷贝大小错误;
-
mipmap layout transition 错误;
-
UV attribute 没有正确传入。
3. 模型变形
可能原因:
-
vertex attribute offset 错误;
-
glTF accessor stride 处理错误;
-
index type 错误;
-
node transform 乘法顺序错误;
-
坐标系处理错误;
-
tangent 没有生成或方向错误;
-
skinning joint index / weight 解析错误。
4. 随机闪烁
可能原因:
-
uniform buffer 被 CPU 提前覆盖;
-
fence 使用错误;
-
多帧资源没有分离;
-
pipeline barrier 不完整;
-
descriptor 指向已释放资源;
-
image layout transition 缺失。
二十六、Vulkan 学习路线
建议学习路线如下。
第一阶段:最小渲染闭环
目标:画出三角形。
需要掌握:
Instance
Device
Queue
Swapchain
Render Pass
Pipeline
Command Buffer
Semaphore
Fence
不要急着加载模型。
先把一帧渲染流程搞清楚。
第二阶段:Buffer 与矩阵
目标:画旋转立方体。
需要掌握:
Vertex Buffer
Index Buffer
Uniform Buffer
Descriptor Set
MVP Matrix
Depth Buffer
这一阶段是从 2D 三角形进入 3D 渲染的关键。
第三阶段:Texture
目标:画带纹理的模型。
需要掌握:
VkImage
VkImageView
VkSampler
Image Layout Transition
Staging Buffer
Combined Image Sampler
Mipmap
sRGB
这一阶段会真正理解 Vulkan 资源管理。
第四阶段:glTF 模型加载
目标:加载并显示 glTF 模型。
需要掌握:
glTF node hierarchy
mesh primitive
accessor / bufferView
material
texture
normal map
PBR
此时你的程序已经接近一个小型 renderer。
第五阶段:工程化
目标:构建可扩展渲染器。
需要掌握:
Frame Resources
Descriptor 分组
Pipeline Cache
VMA
Render Graph
Dynamic Rendering
Compute Shader
GPU Culling
Instancing
Bindless Texture
二十七、一个标准 Vulkan Renderer 的渲染顺序
假设我们要渲染一个 glTF 场景,典型顺序如下:
每帧开始
↓
等待当前帧 fence
↓
获取 swapchain image
↓
更新 camera uniform buffer
↓
更新 light uniform buffer
↓
更新 object transform dynamic buffer
↓
重置 command buffer
↓
开始录制 command buffer
↓
开始 render pass / dynamic rendering
↓
绑定 graphics pipeline
↓
绑定 frame descriptor set
↓
遍历 glTF scene nodes
↓
计算 world matrix
↓
遍历 mesh primitives
↓
绑定 material descriptor set
↓
设置 push constants 或 dynamic offset
↓
绑定 vertex buffer
↓
绑定 index buffer
↓
vkCmdDrawIndexed
↓
结束 render pass
↓
结束 command buffer
↓
提交 queue
↓
present
↓
进入下一帧
这就是 Vulkan 渲染器的主干。
二十八、从教授视角理解 Vulkan:它不是 API,而是 GPU 合约
很多初学者把 Vulkan 当成“更复杂的 OpenGL”。
这是不准确的。
OpenGL 像是一个有大量隐式状态的图形接口。
你调用函数,驱动在背后推断你的意图。
Vulkan 更像是一份明确的 GPU 执行合约。
你必须告诉驱动:
资源是什么?
资源何时读?
资源何时写?
shader 需要哪些绑定?
pipeline 状态是什么?
命令如何组织?
队列如何同步?
图像 layout 如何变化?
这就是 Vulkan 的本质:
不是让 GPU 自动理解你的程序,
而是让你的程序明确描述 GPU 应该如何工作。
二十九、学习 Vulkan 时必须建立的五个核心心智模型
1. 资源不是变量,而是 GPU 对象
在 C++ 中,一个变量可以直接访问。
但在 Vulkan 中,shader 访问资源必须经过:
Buffer / Image
↓
Memory
↓
Descriptor
↓
Pipeline Layout
↓
Shader Binding
2. Pipeline 不是 shader,而是完整渲染状态
Shader 只是 pipeline 的一部分。
Pipeline 还包含:
Vertex Input
Rasterization
Depth Test
Blend
Render Target Format
Pipeline Layout
3. Descriptor Set 是 shader 的资源入口表
如果 shader 要访问 UBO 或 texture,就必须有 descriptor。
没有 descriptor,shader 不知道资源在哪里。
4. Command Buffer 是 CPU 写给 GPU 的任务清单
Vulkan 不鼓励立即模式。
你把命令录制到 command buffer,再提交给 GPU。
5. Synchronization 是正确性的基础
Vulkan 性能高的原因之一,是它不自动插入大量保守同步。
但这也意味着你必须自己保证资源访问顺序正确。
三十、结语:Vulkan 的复杂性来自真实的 GPU 世界
Vulkan 的学习曲线确实陡峭。
但它并不是人为复杂,而是把现代 GPU 渲染中的真实问题暴露给了开发者:
-
显存如何分配;
-
资源如何绑定;
-
shader 如何访问数据;
-
pipeline 如何固定状态;
-
command 如何提交;
-
GPU 如何同步;
-
多帧如何并行;
-
纹理如何转换 layout;
-
模型如何转为 vertex/index/material;
-
渲染结果如何 present 到屏幕。
一旦理解这些问题,Vulkan 就不再是一堆难记的结构体,而是一套非常清晰的 GPU 工作协议。
如果用一句话总结 Vulkan:
Vulkan 是一种显式描述 GPU 渲染与计算工作的现代图形 API。
如果再进一步总结它的工程价值:
Vulkan 让开发者以更高成本换取更高控制力、更低驱动开销、更强多线程能力和更可预测的渲染行为。
对于想深入图形学、游戏引擎、实时渲染、工业可视化、三维模型渲染、GPU 计算和现代渲染架构的人来说,Vulkan 是非常值得系统学习的一门技术。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)