RHI 设计考量:不同 API 差异分析

RHI 设计考量:不同 API 差异分析

摘要:现代图形 API 形成了两条技术路线------以 OpenGL/GLES/DX11 为代表的"驱动托管"路线和以 DX12/Metal/Vulkan 为代表的"显式控制"路线。本文从 Render 与 Compute 两个维度系统对比六大主流 API(OpenGL、OpenGL ES、Direct3D 11、Direct3D 12、Metal、Vulkan)在线程模型、资源管理、绑定方式、管线状态、同步机制、着色器工具链等方面的差异,分析各 API 设计背后的硬件与历史考量,最后落到工程实践:设计一个跨平台 RHI(Render Hardware Interface)时需要考虑的关键问题。

关键字:图形 API;OpenGL;OpenGL ES;Direct3D;Metal;Vulkan;异步计算;RHI;渲染硬件接口;跨平台渲染;描述符;管线状态对象

1 简介

  图形 API 是应用程序与 GPU 驱动之间的接口层。过去十几年里,这个领域发生了一次范式转移:早期 API 把资源分配、状态跟踪、同步、内存驻留等复杂性全部藏在驱动里,应用写起来简单,但 CPU 驱动开销高、行为不可预测;新一代 API 则把这些责任全部交还给应用------代码更冗长,但性能更可控、多线程能力更强。

  理解这些差异不只是为了"选哪个 API",更是为了理解 GPU 的工作方式本身。当你需要写一个跨引擎、跨平台的渲染抽象层(RHI)时,这些差异会直接决定抽象层的形状。RHI 的本质问题可以用一句话概括:用什么形状的接口,才能让所有后端都不扭曲地映射到各自的本地 API?

  本文先给出六大 API 的总览与定位,再从同步机制、资源、线程模型等多个维度逐项对比它们的差异,接着分析各 API 如此设计的背后考量,最后系统梳理 RHI 设计中的关键问题与工程取舍。

2 六大 API 总览

  现代 GPU 虽然采用统一着色器架构,图形与计算共享同一组可编程核心,但二者侧重截然不同:图形任务依赖光栅化器、纹理单元、ROP 等固定功能硬件,沿顶点→光栅化→像素着色的刚性流水线执行,对每帧实时性要求苛刻,内存访问具有较强空间局部性,线程间通信少、同步隐式;而计算任务则完全脱离固定管线,通过计算着色器或 CUDA 等自由定义线程块与共享内存,显式控制寄存器、各级缓存和全局内存的分配与访问,支持组内屏障、原子操作等丰富同步机制,精度范围更广(FP64/FP32/FP16/BF16/INT8 乃至张量核心加速的矩阵运算),且对单次延迟容忍度更高。在现代 API(Vulkan、Direct3D 12、Metal)中,二者可通过异步队列并行执行,使物理模拟、AI 推理等计算负载与图形渲染重叠,从而充分利用硬件资源、降低整体帧时间。

维度 OpenGL OpenGL ES Direct3D 11 Direct3D 12 Metal Vulkan
制定者 Khronos Khronos Microsoft Microsoft Apple Khronos
平台覆盖 桌面全平台(macOS 停在 4.1 且已弃用) 移动/嵌入式/Web(WebGL) Windows/Xbox Windows/Xbox macOS/iOS/tvOS/watchOS Windows/Linux/Android/Switch 等
抽象层级 高(驱动托管) 高(驱动托管) 中高(驱动托管) 低(显式) 低(显式但精简) 低(显式且繁琐)
着色器语言 GLSL ESSL HLSL(DXBC) HLSL(DXIL) MSL SPIR-V
多线程命令录制 几乎不行(单上下文) 不行 有限(Deferred Context) 原生支持 原生支持 原生支持
内存管理 驱动托管 驱动托管 驱动托管 显式(堆/放置资源) 半显式(Heap/Storage Mode) 显式(Memory Type/Heap)
同步机制 隐式为主 隐式为主 隐式为主 显式 Barrier/Fence 自动跟踪 + 手动选项 完全显式 Barrier/Semaphore
入门难度 容易 容易 中等 中等

  可以把这六个 API 排成一条"控制力光谱":GL ≈ GLES < DX11 < Metal ≈ DX12 < Vulkan。越往右,应用承担的责任越多(校验、同步、内存、生命周期),驱动的"魔法"越少,性能越可预测。这条光谱不是线性的------Metal 位于 DX12 和 Vulkan 之间,因为它在提供显式控制的同时保留了部分自动化(如 Hazard Tracking),体现了 Apple 对"开发效率与性能可控"的独特平衡。

3 不同API特性

3.1 OpenGL/OpenGL ES

  OpenGL(Open Graphics Library)是历史悠久的跨平台图形 API,自1992年发布以来一直是图形渲染的事实标准。OpenGL ES(Embedded Systems)是其针对移动和嵌入式设备的精简版本,广泛用于 Android、iOS 和 WebGL。

  OpenGL 采用状态机模型,通过全局状态控制渲染行为,早期以立即模式指定顶点数据,现代核心模式则改用顶点缓冲区对象(VBO)和顶点数组对象(VAO)来管理几何数据。它使用 GLSL 编写顶点、片段及几何着色器,并通过扩展机制支持新硬件特性;内存管理、资源分配和同步等完全由驱动托管,应用无需显式控制底层细节。

  OpenGL 的主要优势在于 API 直观、学习曲线平缓,跨平台支持 Windows、Linux 和 macOS(尽管 macOS 已停更在 4.1),且拥有 RenderDoc 等成熟调试工具和丰富教程。但其劣势也很明显:上下文模型限制多线程命令录制,驱动内部状态跟踪与验证带来较高 CPU 开销,内存管理不透明可能导致碎片和性能波动,同时不同平台版本碎片化严重,功能支持不一致。

  在设计跨平台 RHI 时,OpenGL 后端通常作为"基线"实现,用于兼容旧硬件或快速原型。RHI 需要将 OpenGL 的状态机模型映射到显式 API 的管线状态对象(PSO)模型,这可能需要维护内部状态缓存和延迟提交。由于 OpenGL 的多线程限制,RHI 可能需要将命令录制限制在主线程,或使用辅助上下文进行纹理上传等操作。

3.2 Vulkan

  Vulkan 是 Khronos Group 于 2016 年发布的现代图形 API,旨在提供低开销、跨平台的硬件抽象。它取代了 OpenGL 的地位,成为 Android、Linux 和 Windows 上高性能图形和计算的首选。Vulkan 采用完全显式控制模型,应用负责内存管理、资源状态同步、管线状态构建等所有细节,驱动仅执行最小验证。它支持多线程原生命令录制,命令缓冲区可在多线程上并行录制,显著提升 CPU 利用率。着色器使用 SPIR-V 二进制格式,支持离线编译和跨后端共享。资源绑定通过描述符集(Descriptor Set)实现,支持动态偏移和更新。渲染通道(Render Pass)显式定义,可优化 tile-based GPU 的加载/存储操作。同步原语包括信号量(Semaphore)、栅栏(Fence)、事件(Event)等,提供精细的同步控制。

  Vulkan 的主要优势在于高性能和低驱动开销,适合 CPU 受限的应用(如游戏引擎、实时渲染)。它具有良好的跨平台支持,覆盖 Windows、Linux、Android、Switch 等,避免平台锁定。计算与图形队列可并行执行,支持异步计算。通过扩展机制,Vulkan 支持光线追踪、网格着色器等前沿特性。然而,Vulkan 的复杂度较高,代码量远大于 OpenGL,需要管理大量样板代码(如实例、设备、队列、命令池等)。学习曲线陡峭,概念繁多(如内存类型、队列族、描述符布局),初学者难以掌握。开发时依赖验证层捕获错误,但验证层可能影响性能。不同厂商的驱动质量不一,可能导致跨平台兼容性问题。

  在设计跨平台 RHI 时,Vulkan 是主要目标后端之一。RHI 需要将高层抽象(如资源绑定、同步)映射到 Vulkan 的显式模型。关键挑战包括:管理内存分配(推荐使用 VMA 库)、处理资源状态转换(通过管线屏障)、构建管线状态对象(VkPipeline)、调度异步计算。RHI 通常隐藏 Vulkan 的样板代码,提供简化的资源创建和绑定接口。

3.3 Direct3D 11

  Direct3D 11(D3D11)是微软于 2009 年发布的图形 API,作为 DirectX 家族的一员,长期主导 Windows 和 Xbox 平台的游戏开发。它在易用性和性能之间取得了良好平衡,至今仍被广泛使用。Direct3D 11 采用驱动托管模型,内存管理、资源分配、同步等由驱动自动处理,应用只需调用 API 创建和销毁对象。它支持特性级别(Feature Level)从 9.3 到 11.1,兼容不同代 GPU。着色器使用 HLSL(High-Level Shading Language)编写,编译为 DXBC(DirectX Bytecode)。提供有限的多线程支持,通过延迟上下文(Deferred Context)可将命令录制到延迟上下文,但最终仍需主上下文执行。支持通用计算着色器,但异步计算能力有限。

  Direct3D 11 的主要优势在于易用性,API 设计直观,学习曲线平缓,适合快速开发。经过多年验证,驱动成熟稳定,兼容性好。工具链完善,Visual Studio 图形调试器、PIX 等工具支持良好。广泛支持 Windows 7 及以上系统,Xbox 360/One 向后兼容。然而,其多线程限制明显,Deferred Context 性能提升有限,且不能与 Immediate Context 并行。驱动内部状态跟踪和验证导致 CPU 开销,性能可预测性较低。内存管理不透明,应用无法控制显存分配,可能导致内存碎片和性能波动。相比 Vulkan 和 DX12,缺少光线追踪、网格着色器等新特性。

  在设计跨平台 RHI 时,Direct3D 11 是重要后端,尤其针对 Windows 平台和旧硬件。RHI 需要将高层绑定模型映射到 D3D11 的状态机,例如将描述符集转换为常量缓冲区、纹理和采样器的单独绑定。由于 D3D11 的多线程限制,RHI 通常将命令录制限制在主线程,或使用延迟上下文进行并行录制,但需注意性能开销。

3.4 Direct3D 12

  Direct3D 12(D3D12)是微软于 2015 年发布的低开销图形 API,旨在提供对 GPU 硬件的完全控制,与 Vulkan 和 Metal 竞争。它要求应用显式管理资源、内存和同步,但回报是更高的性能和可预测性。Direct3D 12 采用完全显式控制模型,应用负责内存分配、资源状态管理、同步和管线状态构建。支持多线程原生命令录制,命令列表(Command List)可在多线程上并行录制,显著提升 CPU 利用率。根签名(Root Signature)定义管线使用的资源布局,支持描述符表、根描述符和根常量。管线状态对象(PSO)将所有管线状态(着色器、混合、深度模板等)打包为一个不可变对象,减少状态切换开销。显式内存管理通过堆(Heap)和放置资源(Placed Resource)管理显存,支持内存别名。计算队列与图形队列并行执行,支持异步计算和复制。

  Direct3D 12 的主要优势在于高性能,低驱动开销,适合 CPU 受限的应用(如 AAA 游戏、实时渲染)。支持 Windows 10 及以上和 Xbox One 系列,具有良好的跨平台能力。计算与图形队列可并行执行,支持异步计算。通过 DirectX Raytracing(DXR)和网格着色器等前沿特性,提供先进的图形功能。然而,Direct3D 12 的复杂度较高,代码量远大于 D3D11,需要管理大量样板代码(如命令队列、命令分配器、围栏等)。学习曲线陡峭,概念繁多(如堆类型、根签名、描述符堆),初学者难以掌握。开发时依赖调试层捕获错误,但调试层可能影响性能。平台限制明显,仅支持 Windows 10 及以上和 Xbox,跨平台能力有限。

  在设计跨平台 RHI 时,Direct3D 12 是主要目标后端之一,尤其针对 Windows 和 Xbox 平台。RHI 需要将高层抽象映射到 D3D12 的显式模型,关键挑战包括:管理内存分配(推荐使用 D3D12MA 库)、处理资源状态转换(通过资源屏障)、构建管线状态对象(PSO)、调度异步计算。RHI 通常隐藏 D3D12 的样板代码,提供简化的资源创建和绑定接口。

3.5 Metal

  Metal 是 Apple 于 2014 年发布的低开销图形和计算 API,专为 macOS、iOS、tvOS 和 watchOS 设计。它提供了对 Apple 硬件的深度优化,同时保持了相对简洁的 API 设计。Metal 采用半显式控制模型,应用负责资源创建和存储模式选择,但内存管理和同步部分由驱动自动处理(如 Hazard Tracking)。支持多线程原生命令录制,命令缓冲区(Command Buffer)可在多线程上并行录制,支持并行渲染和计算。着色器使用 Metal Shading Language(MSL),基于 C++ 语法,支持内核函数(Kernel Function)用于计算。资源堆(Heap)允许应用创建堆并在其中分配资源,支持内存别名和子分配。存储模式(Storage Mode)控制资源内存位置和 CPU 可见性(共享、私有、托管)。渲染通道(Render Pass)显式定义附件操作,优化 tile-based GPU 的加载/存储。

  Metal 的主要优势在于 Apple 生态深度集成,与 Core Animation、Core Image、ARKit 等框架无缝协作。针对 Apple 硬件(如 A 系列、M 系列芯片)深度优化,充分利用 tile-based 架构,提供卓越的性能。相比 Vulkan 和 DX12,API 设计更简洁,学习曲线较平缓。计算队列与图形队列共享内存,避免数据拷贝,实现统一计算与图形。然而,Metal 平台锁定明显,仅支持 Apple 平台,无法跨平台。生态限制较大,依赖 Apple 的工具链(Xcode)和更新周期。文档和社区规模较小,相比 Vulkan 和 DirectX 资源有限。macOS 上的 Metal 功能集略落后于 iOS(如光线追踪支持较晚)。

  在设计跨平台 RHI 时,Metal 是关键后端,尤其针对 Apple 平台。RHI 需要将高层抽象映射到 Metal 的半显式模型,关键挑战包括:管理存储模式(共享、私有、托管)、处理资源状态(依赖 Hazard Tracking 或手动屏障)、构建渲染通道描述符、调度异步计算。RHI 通常隐藏 Metal 的样板代码,提供简化的资源创建和绑定接口,并利用 Apple 的框架集成(如 CAMetalLayer)。

4 不同维度对比不同API

4.1 资源管理与显存控制

  资源管理与显存控制是图形 API 差异最显著的领域之一。旧一代 API 将显存分配、驻留、生命周期完全交给驱动,开发者只需调用 glGenTexturesglBufferData 等接口,驱动在后台完成所有簿记工作;新一代 API 则将内存管理的责任完全下放给应用,开发者必须显式地选择内存类型、创建堆、管理资源状态,甚至处理 GPU 前后端内存的可见性。这种转变带来了更高的性能上限和可预测性,但也大幅增加了代码复杂度。

4.1.1 内存管理模型对比
API 内存管理模型 分配单元 驱动职责 应用职责
OpenGL 完全驱动托管 无显式概念 所有分配、驻留、回收 仅调用 API 创建/销毁对象
OpenGL ES 完全驱动托管 无显式概念 同 OpenGL 同 OpenGL
Direct3D 11 驱动托管 + 分类提示 无显式堆 所有分配、驻留、回收;根据 D3D11_USAGE 提示优化 指定用途(默认/上传/读回)
Direct3D 12 完全显式 堆(Heap) 分配物理显存页 创建堆、选择内存类型、管理驻留
Metal 半显式 堆(Heap) 管理物理内存 选择存储模式(共享/私有/托管)
Vulkan 完全显式 内存堆(Memory Heap) 暴露可用内存类型 选择内存类型、分配内存块、绑定资源

  在显式 API 中,内存管理的核心流程是:查询可用内存类型 → 创建堆/分配内存 → 创建资源并绑定内存 → 管理资源状态与驻留。这一流程在 DX12、Metal、Vulkan 中大体一致,但细节差异显著。

4.1.2 显存分配与堆管理
4.1.2.1 Direct3D 12 的堆与放置资源

  DX12 引入了 ID3D12Heap 接口,应用可以创建一个大堆,然后在堆内放置多个资源(Placed Resources)。堆本身有 D3D12_HEAP_TYPE 分类:

堆类型 显存位置 CPU 访问 典型用途
D3D12_HEAP_TYPE_DEFAULT 显存(VRAM) 不可直接访问 纹理、缓冲区、渲染目标
D3D12_HEAP_TYPE_UPLOAD 系统内存 可写 上传缓冲区(纹理/缓冲区数据)
D3D12_HEAP_TYPE_READBACK 系统内存 可读 GPU 回读(查询、截图)
D3D12_HEAP_TYPE_CUSTOM 自定义 自定义 高级用法(多适配器等)
cpp 复制代码
// DX12: 创建堆并放置资源
D3D12_HEAP_PROPERTIES heapProps = {};
heapProps.Type = D3D12_HEAP_TYPE_DEFAULT;

D3D12_HEAP_DESC heapDesc = {};
heapDesc.SizeInBytes = 256 * 1024 * 1024; // 256MB
heapDesc.Properties = heapProps;
heapDesc.Flags = D3D12_HEAP_FLAG_ALLOW_ALL_BUFFERS_AND_TEXTURES;

ComPtr<ID3D12Heap> heap;
device->CreateHeap(&heapDesc, IID_PPV_ARGS(&heap));

// 在堆内place new纹理
D3D12_RESOURCE_DESC texDesc = CD3DX12_RESOURCE_DESC::Texture2D(
    DXGI_FORMAT_R8G8B8A8_UNORM, 1024, 1024);

D3D12_RESOURCE_ALLOCATION_INFO allocInfo =
    device->GetResourceAllocationInfo(0, 1, &texDesc);

ComPtr<ID3D12Resource> texture;
device->CreatePlacedResource(
    heap.Get(),
    D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT, 
    &texDesc,
    D3D12_RESOURCE_STATE_COMMON,
    nullptr,
    IID_PPV_ARGS(&texture));

  放置资源的关键优势是减少内存碎片和分配开销。应用可以预分配一个大堆,然后将生命周期相近的资源放在同一堆中,避免频繁的驱动分配调用。

4.1.2.2 Vulkan 的内存类型与堆

  Vulkan 通过 vkGetPhysicalDeviceMemoryProperties 查询设备支持的内存类型和内存堆。每个内存类型关联一个内存堆(物理显存区域),并具有特定的属性标志:

属性标志 含义 典型用途
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT 仅 GPU 可访问 纹理、渲染目标、GPU 专用缓冲区
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT CPU 可映射 上传/回读缓冲区
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT CPU 写入自动对 GPU 可见 无需显式刷新映射
VK_MEMORY_PROPERTY_HOST_CACHED_BIT CPU 缓存加速读取 频繁回读的数据
VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT 按需分配 临时渲染目标(移动端 tile 内存)
VK_MEMORY_PROPERTY_PROTECTED_BIT 受保护内存 DRM 内容
cpp 复制代码
// Vulkan: 查询内存类型
VkPhysicalDeviceMemoryProperties memProps;
vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProps);

// 寻找设备本地且主机可见的内存类型(用于统一内存架构)
uint32_t unifiedMemType = UINT32_MAX;
for (uint32_t i = 0; i < memProps.memoryTypeCount; i++) {
    if ((memProps.memoryTypes[i].propertyFlags &
         VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) &&
        (memProps.memoryTypes[i].propertyFlags &
         VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT)) {
        unifiedMemType = i;
        break;
    }
}

// 分配缓冲区内存
VkMemoryAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memReqs.size;
allocInfo.memoryTypeIndex = unifiedMemType;

VkDeviceMemory bufferMemory;
vkAllocateMemory(device, &allocInfo, nullptr, &bufferMemory);
vkBindBufferMemory(device, buffer, bufferMemory, 0);

  Vulkan 的内存模型允许应用实现子分配(Sub-allocation):分配一大块内存,然后通过偏移量将不同资源绑定到同一内存块。这是性能优化的关键手段,但需要应用自行管理对齐和屏障。

4.1.2.3 Metal 的堆与存储模式

  Metal 提供了三种存储模式,控制纹理和缓冲区的内存位置与 CPU 可见性:

存储模式 内存位置 CPU 访问 典型用途
MTLResourceStorageModeShared 统一内存架构:系统内存;离散架构:驱动选择 CPU 和 GPU 均可访问 频繁 CPU 更新的数据
MTLResourceStorageModePrivate 仅 GPU 可访问 不可直接访问 GPU 专用资源(纹理、渲染目标)
MTLResourceStorageModeManaged 系统内存(CPU)+ GPU 缓存 CPU 可映射,需手动同步 macOS 上的高效 CPU/GPU 共享
swift 复制代码
// Metal: 创建堆并分配资源
let device = MTLCreateSystemDefaultDevice()!

// 创建私有堆(GPU 专用)
let heapDescriptor = MTLHeapDescriptor()
heapDescriptor.size = 256 * 1024 * 1024  // 256MB
heapDescriptor.storageMode = .private
heapDescriptor.hazardTrackingMode = .tracked

let heap = device.makeHeap(descriptor: heapDescriptor)!

// 在堆中创建纹理
let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(
    pixelFormat: .rgba8Unorm,
    width: 1024, height: 1024,
    mipmapped: false)
textureDescriptor.storageMode = .private
textureDescriptor.usage = [.shaderRead, .renderTarget]

let texture = heap.makeTexture(descriptor: textureDescriptor)!

  Metal 的堆管理比 DX12 和 Vulkan 更简洁,应用只需关注存储模式,驱动会处理内存对齐和页管理。

4.1.3 资源状态管理

  在显式 API 中,资源状态(Resource State)管理是同步的基础。GPU 在不同阶段以不同方式访问资源,应用必须在访问前将资源转换到正确的状态。

API 状态管理方式 状态粒度 状态转换
Direct3D 11 驱动自动处理 无显式状态 自动(隐式同步)
Direct3D 12 显式屏障 资源整体或子资源 ResourceBarrier 显式转换
Metal 自动跟踪 + 手动选项 资源整体 Hazard Tracking 自动检测冲突
Vulkan 显式管线屏障 资源整体或子资源 vkCmdPipelineBarrier 显式转换
cpp 复制代码
// DX12: 资源状态转换
D3D12_RESOURCE_BARRIER barrier = {};
barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
barrier.Transition.pResource = texture.Get();
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST;
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;
barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;

commandList->ResourceBarrier(1, &barrier);
cpp 复制代码
// Vulkan: 管线屏障(图像布局转换)
VkImageMemoryBarrier barrier = {};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = image;
barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
barrier.subresourceRange.levelCount = 1;
barrier.subresourceRange.layerCount = 1;
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;

vkCmdPipelineBarrier(
    commandBuffer,
    VK_PIPELINE_STAGE_TRANSFER_BIT,
    VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
    0, 0, nullptr, 0, nullptr, 1, &barrier);

  Metal 的 Hazard Tracking 机制是独特的折中:驱动在后台跟踪资源访问冲突,当检测到 Hazard 时自动插入屏障。应用可以选择完全依赖自动跟踪(简化代码),或手动插入屏障以获得更高性能。

4.1.4 资源别名与内存复用

  在资源生命周期不重叠的情况下,多个资源可以共享同一块内存,这就是资源别名(Resource Aliasing)。这对减少显存占用至关重要。

API 别名支持 实现方式
Direct3D 12 原生支持 创建放置资源时使用同一堆,通过屏障管理访问
Metal 原生支持 使用 makeAliasable() 标记资源可被别名
Vulkan 原生支持 稀疏绑定(Sparse Binding)或资源别名(Memory Aliasing)
OpenGL/DX11 不直接支持 驱动可能内部优化,但应用无法控制
swift 复制代码
// Metal: 资源别名
let texture1 = heap.makeTexture(descriptor: desc1)!
texture1.makeAliasable()  // 标记为可别名

let texture2 = heap.makeTexture(descriptor: desc2)!  // 复用同一内存
// texture1 和 texture2 不会同时使用
4.1.5 跨平台 RHI 的资源管理策略

  设计跨平台 RHI 时,资源管理需要在不同 API 之间找到最大公约数。常见的策略包括:

4.1.5.1 统一内存模型抽象

  RHI 可以定义一套内存类型枚举,映射到各后端的具体内存类型:

cpp 复制代码
enum class RHIMemoryType {
    DeviceLocal,      // GPU 专用显存
    HostVisible,      // CPU 可映射内存(上传/回读)
    HostCoherent,     // CPU 写入自动对 GPU 可见
    Unified,          // 统一内存(设备本地 + 主机可见)
    LazilyAllocated,  // 按需分配(移动端 tile 内存)
};
4.1.5.2 资源分配器(Resource Allocator)

  工程实践中通常使用子分配器减少显存分配次数。流行的库包括:

库名 支持 API 特点
VMA (Vulkan Memory Allocator) Vulkan 轻量、子分配、别名管理
D3D12MA (D3D12 Memory Allocator) DX12 与 VMA 接口一致
MetalKit Metal Apple 官方,简单易用
AMD GPUOpen Memory Allocator DX12/Vulkan 跨 API

  这些分配器提供统一的接口,隐藏各后端的内存管理细节,是 RHI 设计的重要基础设施。

4.1.5.3 资源生命周期管理

  资源销毁需要确保 GPU 不再使用该资源。不同 API 的策略不同:

API 销毁策略
OpenGL 调用 glDelete*,驱动延迟回收
Direct3D 11 调用 Release(),驱动延迟回收
Direct3D 12 必须确保 GPU 完成使用后才能销毁,通常通过围栏(Fence)
Metal 引用计数,GPU 使用完自动释放
Vulkan 必须显式销毁,需等待设备空闲或使用删除队列

  RHI 通常实现延迟删除队列:将待销毁资源放入队列,每帧检查 GPU 完成状态,安全释放已不再使用的资源。

4.1.6 移动端优化考量

  移动 GPU(如 ARM Mali、Qualcomm Adreno、Apple A/M 系列)通常采用 Tile-Based Deferred Rendering (TBDR) 架构,显存管理有特殊考量:

特性 影响 RHI 应对
统一内存架构 CPU 和 GPU 共享物理内存 使用 HostVisible + DeviceLocal 内存类型
Tile 内存 片上小容量高速内存 使用 LazilyAllocated 内存,优化 tile 利用率
带宽限制 系统内存带宽有限 减少纹理格式大小,使用压缩纹理(ASTC/ETC)
功耗敏感 内存访问影响功耗 优化内存局部性,减少随机访问

  在 Vulkan 中,VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT 允许驱动使用 tile 内存而非系统内存存储渲染目标,这对移动端性能至关重要。Metal 的 MTLResourceStorageModePrivate 在 iOS 上默认使用 tile 内存。

4.1.7 小结

  资源管理与显存控制是显式 API 复杂性的主要来源。从 OpenGL 的"完全托管"到 Vulkan 的"完全显式",开发者承担的责任逐步增加,但性能上限和可预测性也同步提升。RHI 设计的关键是在这些差异之上建立统一抽象,同时保留各后端的优化路径。子分配器、延迟删除、资源别名等模式是工程实践中的标准解决方案。

5 多线程命令录制与提交

  多线程命令录制是现代图形 API 提升 CPU 利用率的关键特性。旧一代 API(OpenGL、GLES、DX11)的多线程支持有限,而新一代 API(DX12、Metal、Vulkan)则提供了原生的多线程命令录制能力。这种差异直接影响 RHI 的设计:RHI 必须提供统一的多线程抽象,同时处理各后端的实现细节。

5.1 多线程模型对比

API 多线程支持 命令录制模型 命令提交模型 资源绑定模型
OpenGL 几乎不行 单上下文,单线程 单上下文提交 全局状态,单线程
OpenGL ES 几乎不行 单上下文,单线程 单上下文提交 全局状态,单线程
Direct3D 11 有限(Deferred Context) 延迟上下文可多线程录制 主上下文提交 全局状态,单线程
Direct3D 12 原生支持 命令列表可多线程录制 命令队列提交 描述符堆,多线程
Metal 原生支持 命令缓冲区可多线程录制 命令队列提交 参数缓冲区,多线程
Vulkan 原生支持 命令缓冲区可多线程录制 队列提交 描述符集,多线程

5.2 命令录制模型

5.2.1 OpenGL 的单线程限制

  OpenGL 使用上下文(Context)模型,一个上下文绑定到一个线程。虽然可以通过共享上下文在多线程间共享纹理和缓冲区,但命令录制必须在主线程进行。这导致 CPU 利用率低下,尤其是在复杂场景中。

cpp 复制代码
// OpenGL: 单线程命令录制
glBindTexture(GL_TEXTURE_2D, texture);
glDrawArrays(GL_TRIANGLES, 0, 3);
// 所有 OpenGL 调用必须在同一线程
5.2.2 Direct3D 11 的延迟上下文

  DX11 引入了延迟上下文(Deferred Context),允许在多线程上录制命令,但最终必须通过主上下文(Immediate Context)执行。延迟上下文录制的命令列表可以提交给立即上下文执行,但存在性能开销。

cpp 复制代码
// DX11: 延迟上下文多线程录制
ID3D11DeviceContext* deferredContext;
device->CreateDeferredContext(0, &deferredContext);

// 在子线程录制命令
deferredContext->Draw(3, 0);

// 获取命令列表
ID3D11CommandList* commandList;
deferredContext->FinishCommandList(FALSE, &commandList);

// 主线程执行命令列表
immediateContext->ExecuteCommandList(commandList, TRUE);
5.2.3 Direct3D 12 的命令列表

  DX12 使用命令列表(Command List)模型,每个命令列表可以独立录制,无需同步。多个线程可以同时录制不同的命令列表,最后提交到命令队列。命令列表是轻量级对象,可以快速创建和重置。

cpp 复制代码
// DX12: 多线程命令录制
// 每个线程有独立的命令分配器和命令列表
struct ThreadContext {
    ComPtr<ID3D12CommandAllocator> allocator;
    ComPtr<ID3D12GraphicsCommandList> commandList;
};

// 子线程录制命令
void RenderThread(ThreadContext& ctx) {
    ctx.allocator->Reset();
    ctx.commandList->Reset(ctx.allocator.Get(), nullptr);
    
    // 录制渲染命令
    ctx.commandList->DrawInstanced(3, 1, 0, 0);
    
    ctx.commandList->Close();
}

// 主线程提交所有命令列表
std::vector<ID3D12CommandList*> commandLists;
for (auto& ctx : threadContexts) {
    commandLists.push_back(ctx.commandList.Get());
}
commandQueue->ExecuteCommandLists(commandLists.size(), commandLists.data());
5.2.4 Metal 的命令缓冲区

  Metal 使用命令缓冲区(Command Buffer)模型,每个命令缓冲区可以从命令队列创建,并在单一线程上录制;多个线程可各自创建并录制不同的命令缓冲区以实现并行。

swift 复制代码
// Metal: 多线程命令录制
let commandBuffer = commandQueue.makeCommandBuffer()!

// 在任意线程录制命令
let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)!
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
encoder.endEncoding()

// 提交命令缓冲区
commandBuffer.commit()
5.2.5 Vulkan 的命令缓冲区

  Vulkan 使用命令缓冲区(Command Buffer)模型,每个命令缓冲区可以从命令池(Command Pool)创建。命令池与队列族关联,不同线程可以使用不同的命令池并行录制。命令缓冲区录制完成后提交到队列。

cpp 复制代码
// Vulkan: 多线程命令录制
// 每个线程有自己的命令池
VkCommandPoolCreateInfo poolInfo = {};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; 
poolInfo.queueFamilyIndex = graphicsQueueFamily;
vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool);

// 分配命令缓冲区
VkCommandBuffer commandBuffer;
VkCommandBufferAllocateInfo allocInfo = {};
allocInfo.commandPool = commandPool;
allocInfo.commandBufferCount = 1;
vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer);

// 录制命令
vkBeginCommandBuffer(commandBuffer, &beginInfo);
vkCmdDraw(commandBuffer, 3, 1, 0, 0);
vkEndCommandBuffer(commandBuffer);

// 提交到队列
VkSubmitInfo submitInfo = {};
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;
vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);

5.3 命令提交与队列

  命令提交模型决定了命令如何被 GPU 执行。不同 API 提供了不同粒度的队列和提交机制。

API 队列模型 提交粒度 异步执行
OpenGL 单一上下文队列 隐式批量提交 驱动控制
Direct3D 11 单一命令队列 隐式批量提交 驱动控制
Direct3D 12 多队列(图形/计算/复制) 显式命令列表提交 显式控制
Metal 多队列(图形/计算) 显式命令缓冲区提交 显式控制
Vulkan 多队列(图形/计算/传输) 显式命令缓冲区提交 显式控制

5.4 资源绑定与多线程

  在多线程环境下,资源绑定需要线程安全。不同 API 提供了不同的绑定模型。

API 绑定模型 线程安全 多线程绑定策略
OpenGL 全局状态 单线程绑定
Direct3D 11 全局状态 单线程绑定
Direct3D 12 描述符堆 + 根签名 描述符堆本身非线程安全,需每线程独立
Metal 参数缓冲区 每个线程独立参数缓冲区
Vulkan 描述符集 每个线程独立描述符集

5.5 同步与屏障

  多线程命令录制引入了新的同步需求:线程间同步、命令列表间同步、GPU-CPU 同步。不同 API 提供了不同的同步机制。

API 线程间同步 命令列表间同步 GPU-CPU 同步
OpenGL 不适用 驱动内部同步 隐式同步
Direct3D 11 不适用 驱动内部同步 隐式同步
Direct3D 12 应用显式同步 围栏(Fence) 围栏(Fence)
Metal 驱动自动跟踪 事件(Event) 事件(Event)
Vulkan 应用显式同步 信号量(Semaphore) 栅栏(Fence)

5.6 跨平台 RHI 的多线程策略

  设计跨平台 RHI 时,多线程抽象需要平衡各后端的差异。常见的策略包括:

5.6.1 统一命令录制模型

  RHI 可以定义统一的命令列表(Command List)接口,映射到各后端的具体实现:

cpp 复制代码
class RHICommandList {
public:
    virtual void Begin() = 0;
    virtual void End() = 0;
    virtual void Draw(uint32_t vertexCount, uint32_t instanceCount) = 0;
    virtual void Dispatch(uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ) = 0;
};
5.6.2 线程本地存储

  RHI 可以使用线程本地存储(TLS)为每个线程维护独立的命令录制上下文:

cpp 复制代码
class RHIContext {
    static thread_local RHICommandList* currentCommandList;
public:
    static void SetCurrentCommandList(RHICommandList* list) {
        currentCommandList = list;
    }
    static RHICommandList& GetCurrentCommandList() {
        return *currentCommandList;
    }
};
5.6.3 命令队列抽象

  RHI 可以定义统一的命令队列接口,支持图形、计算和传输队列:

cpp 复制代码
enum class RHIQueueType {
    Graphics,
    Compute,
    Transfer,
};

class RHICommandQueue {
public:
    virtual void Submit(RHICommandList& commandList) = 0;
    virtual void WaitForIdle() = 0;
};

5.7 小结

  多线程命令录制是现代图形 API 的核心特性,直接决定了 CPU 利用率和渲染性能。OpenGL 和 DX11 的多线程支持有限,而 DX12、Metal 和 Vulkan 提供了原生的多线程能力。RHI 设计的关键是在这些差异之上建立统一的多线程抽象,同时保留各后端的优化路径。线程本地存储、统一命令列表和队列抽象是工程实践中的标准解决方案。

6 同步机制与屏障

  同步机制是图形 API 中确保数据一致性和执行顺序的关键部分。在 GPU 并行执行的环境下,应用必须显式或隐式地管理 CPU-GPU 同步、GPU-GPU 同步、内存可见性和资源状态转换。不同 API 提供了不同粒度的同步原语,直接影响 RHI 的设计。

6.1 同步机制对比

API CPU-GPU 同步 GPU-GPU 同步 内存可见性 资源状态管理
OpenGL 隐式(glFinish/glFenceSync 隐式(驱动内部) 隐式 驱动自动
Direct3D 11 隐式(Map/Unmap 隐式(驱动内部) 隐式 驱动自动
Direct3D 12 显式围栏(Fence) 显式围栏(Fence) 显式屏障 显式资源屏障
Metal 显式(等待命令缓冲完成) 显式事件(Event) 自动跟踪 + 手动选项 Hazard Tracking
Vulkan 显式栅栏(Fence) 显式信号量(Semaphore) 显式内存屏障 显式管线屏障

6.2 栅栏与围栏(Fence)

  栅栏(Fence)是最基本的同步原语,用于 CPU-GPU 同步和 GPU-GPU 同步。应用在命令队列中插入栅栏信号,然后等待或轮询栅栏状态。

6.2.1 Direct3D 12 的围栏

  DX12 使用 ID3D12Fence 接口,支持 CPU-GPU 同步和多队列同步。围栏有一个单调递增的计数器,GPU 完成指定操作后递增计数器,CPU 可以等待计数器达到特定值。

cpp 复制代码
ComPtr<ID3D12Fence> fence;
device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence));
uint64_t fenceValue = 0;

// 在命令队列中插入围栏信号(假设命令已提交)
commandQueue->Signal(fence.Get(), ++fenceValue);

// CPU 等待 GPU 完成
HANDLE eventHandle = CreateEvent(nullptr, FALSE, FALSE, nullptr);
fence->SetEventOnCompletion(fenceValue, eventHandle);
WaitForSingleObject(eventHandle, INFINITE);
CloseHandle(eventHandle);
6.2.2 Metal 的事件

  Metal 使用 MTLEvent 接口,支持 CPU-GPU 同步和多队列同步。事件可以与命令缓冲区关联,当命令缓冲区完成时触发事件。

swift 复制代码
// CPU-GPU 同步
let commandBuffer = commandQueue.makeCommandBuffer()!
// 录制命令...
commandBuffer.commit()
commandBuffer.waitUntilCompleted() // CPU 等待 GPU 完成

// GPU-GPU 同步(跨命令缓冲区)
let event = device.makeEvent()!
let commandBuffer1 = commandQueue.makeCommandBuffer()!
let encoder1 = commandBuffer1.makeBlitCommandEncoder()!
encoder1.encodeSignalEvent(event, value: 1)
encoder1.endEncoding()
commandBuffer1.commit()

let commandBuffer2 = commandQueue.makeCommandBuffer()!
let encoder2 = commandBuffer2.makeBlitCommandEncoder()!
encoder2.encodeWaitForEvent(event, value: 1)
encoder2.endEncoding()
commandBuffer2.commit()
6.2.3 Vulkan 的栅栏

  Vulkan 使用 VkFence 接口,支持 CPU-GPU 同步。栅栏与队列提交关联,当队列中的所有命令执行完成后,驱动递增栅栏的信号值。

cpp 复制代码
// Vulkan: 栅栏同步
VkFence fence;
VkFenceCreateInfo fenceInfo = {};
vkCreateFence(device, &fenceInfo, nullptr, &fence);

// 提交命令并关联栅栏
vkQueueSubmit(queue, 1, &submitInfo, fence);

// CPU 等待 GPU 完成
vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX);
vkResetFences(device, 1, &fence);

6.3 信号量与事件(Semaphore & Event)

  信号量(Semaphore)用于 GPU-GPU 同步,确保不同队列或不同命令缓冲区之间的执行顺序。事件(Event)用于更细粒度的同步,可以在命令流中插入同步点。

6.3.1 Vulkan 的信号量

  Vulkan 使用 VkSemaphore 接口,支持图形队列与计算队列、呈现队列之间的同步。信号量在队列提交时信号,在另一个队列等待。

cpp 复制代码
// Vulkan: 信号量同步
VkSemaphore imageAvailableSemaphore;
VkSemaphore renderFinishedSemaphore;

// 图形队列等待图像可用信号,然后信号渲染完成
VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = &imageAvailableSemaphore;
submitInfo.pWaitDstStageMask = waitStages;
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = &renderFinishedSemaphore;

vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);

// 呈现队列等待渲染完成信号
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = &renderFinishedSemaphore;
vkQueuePresentKHR(presentQueue, &presentInfo);
6.3.2 Vulkan 的事件

  Vulkan 使用 VkEvent 接口,支持命令流内的细粒度同步。事件可以在命令缓冲区中设置和等待,用于依赖不同命令流的结果。

cpp 复制代码
// Vulkan: 事件同步
VkEvent event;
VkEventCreateInfo eventInfo = {};
vkCreateEvent(device, &eventInfo, nullptr, &event);

// 在命令缓冲区中设置事件
vkCmdSetEvent(commandBuffer, event, VK_PIPELINE_STAGE_TRANSFER_BIT);

// 在另一个命令缓冲区中等待事件
vkCmdWaitEvents(commandBuffer, 1, &event,
    VK_PIPELINE_STAGE_TRANSFER_BIT,
    VK_PIPELINE_STAGE_VERTEX_INPUT_BIT,
    0, nullptr, 0, nullptr, 0, nullptr);

6.4 内存屏障与管线屏障

  内存屏障(Memory Barrier)确保内存操作的顺序性和可见性。管线屏障(Pipeline Barrier)确保命令执行的顺序性,并可以插入内存屏障。

6.4.1 Direct3D 12 的资源屏障

  DX12 使用资源屏障(Resource Barrier)管理资源状态转换和内存依赖。屏障类型包括状态转换屏障、别名屏障和 UAV 屏障。

cpp 复制代码
// DX12: 资源屏障
D3D12_RESOURCE_BARRIER barrier = {};
barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
barrier.Transition.pResource = texture.Get();
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST;
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;
barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;

commandList->ResourceBarrier(1, &barrier);
6.4.2 Vulkan 的管线屏障

  Vulkan 使用管线屏障(Pipeline Barrier)管理命令执行顺序和内存依赖。屏障可以指定源阶段和目标阶段,以及内存依赖的访问类型。

cpp 复制代码
// Vulkan: 管线屏障
VkMemoryBarrier memoryBarrier = {};
memoryBarrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER;
memoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
memoryBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;

vkCmdPipelineBarrier(
    commandBuffer,
    VK_PIPELINE_STAGE_TRANSFER_BIT,
    VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
    0, 1, &memoryBarrier, 0, nullptr, 0, nullptr);
6.4.3 Metal 的 Hazard Tracking

  Metal 使用 Hazard Tracking 机制自动检测资源访问冲突。驱动在后台跟踪资源使用状态,当检测到 Hazard 时自动插入屏障。应用也可以手动插入屏障以优化性能。

swift 复制代码
// Metal: 使用 MTLFence 手动同步
let fence = device.makeFence()!

let encoder1 = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)!
// 渲染到纹理...
encoder1.endEncoding()

// 在第二个编码器前等待第一个编码器的写入完成
let encoder2 = commandBuffer.makeBlitCommandEncoder()!
encoder2.waitForFence(fence, before: .fragment) // 假设后续使用片段着色器读取
encoder2.endEncoding()

6.5 资源状态转换

  资源状态转换是同步的重要组成部分,确保资源在不同使用阶段处于正确的状态。不同 API 提供了不同粒度的状态管理。

API 状态粒度 状态转换方式 状态跟踪
OpenGL 无显式状态 驱动自动 驱动自动
Direct3D 11 无显式状态 驱动自动 驱动自动
Direct3D 12 资源整体或子资源 显式资源屏障 应用手动
Metal 资源整体 Hazard Tracking 自动 + 手动选项 驱动自动 + 应用手动
Vulkan 资源整体或子资源 显式管线屏障 应用手动

6.6 跨平台 RHI 的同步策略

  设计跨平台 RHI 时,同步抽象需要平衡各后端的差异。常见的策略包括:

6.6.1 统一同步原语

  RHI 可以定义统一的栅栏和信号量接口,映射到各后端的具体实现:

cpp 复制代码
class RHIFence {
public:
    virtual void Signal(uint64_t value) = 0;
    virtual void Wait(uint64_t value) = 0;
    virtual bool IsComplete(uint64_t value) = 0;
};

class RHISemaphore {
public:
    // 由驱动管理内部状态
};
6.6.2 自动状态转换

  RHI 可以维护资源状态表,自动插入必要的状态转换屏障:

cpp 复制代码
class RHIResource {
    RHIResourceState currentState;
public:
    void TransitionTo(RHICommandList& cmdList, RHIResourceState newState) {
        if (currentState != newState) {
            cmdList.InsertBarrier(this, currentState, newState);
            currentState = newState;
        }
    }
};
6.6.3 延迟同步

  RHI 可以延迟同步操作,批量处理多个屏障,减少 API 调用开销:

cpp 复制代码
class RHICommandList {
    std::vector<RHIBarrier> pendingBarriers;
public:
    void InsertBarrier(RHIResource* resource, RHIResourceState before, RHIResourceState after) {
        pendingBarriers.push_back({resource, before, after});
    }
    void FlushBarriers() {
        // 批量提交所有挂起的屏障
        pendingBarriers.clear();
    }
};

6.7 小结

  同步机制是图形 API 中确保数据一致性和执行顺序的关键部分。OpenGL 和 DX11 使用隐式同步,而 DX12、Metal 和 Vulkan 提供显式同步原语。RHI 设计的关键是在这些差异之上建立统一的同步抽象,同时保留各后端的优化路径。自动状态转换、延迟同步和统一同步原语是工程实践中的标准解决方案。

7 管线状态与着色器架构

  管线状态与着色器架构是图形 API 的核心部分,决定了渲染管线的配置和着色器的执行方式。旧一代 API 使用全局状态机模型,而新一代 API 则采用不可变的管线状态对象(PSO)模型。这种转变提高了性能,但也增加了复杂度。

7.1 管线状态模型对比

API 管线状态模型 状态切换开销 着色器绑定模型
OpenGL 全局状态机 高(驱动验证) 随时绑定着色器程序
Direct3D 11 全局状态机 中(驱动验证) 随时绑定着色器
Direct3D 12 管线状态对象(PSO) 低(预编译) 根签名 + 描述符堆
Metal 管线状态对象(PSO) 低(预编译) 参数缓冲区
Vulkan 管线状态对象(PSO) 低(预编译) 描述符集 + 管线布局

7.2 管线状态对象(PSO)

  管线状态对象(Pipeline State Object, PSO)是现代图形 API 的核心概念。PSO 将所有管线状态(着色器、混合、深度模板、光栅化等)打包为一个不可变对象,驱动可以提前编译和优化。

7.2.1 Direct3D 12 的 PSO

  DX12 使用 ID3D12PipelineState 接口,包含图形 PSO 和计算 PSO。PSO 在创建时绑定所有着色器和状态,之后可以快速切换。

cpp 复制代码
// DX12: 创建图形 PSO
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
psoDesc.InputLayout = { inputLayout, numInputLayoutElements };
psoDesc.pRootSignature = rootSignature.Get();
psoDesc.VS = { vertexShaderBlob->GetBufferPointer(), vertexShaderBlob->GetBufferSize() };
psoDesc.PS = { pixelShaderBlob->GetBufferPointer(), pixelShaderBlob->GetBufferSize() };
psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
psoDesc.SampleMask = UINT_MAX;
psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
psoDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
psoDesc.NumRenderTargets = 1;
psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
psoDesc.SampleDesc.Count = 1;

ComPtr<ID3D12PipelineState> pso;
device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pso));

// 绑定 PSO
commandList->SetPipelineState(pso.Get());
7.2.2 Metal 的 PSO

  Metal 使用 MTLRenderPipelineStateMTLComputePipelineState 接口。PSO 在创建时编译着色器,之后可以快速切换。

swift 复制代码
// Metal: 创建图形 PSO
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm

let pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor)

// 绑定 PSO
renderEncoder.setRenderPipelineState(pipelineState)
7.2.3 Vulkan 的 PSO

  Vulkan 使用 VkPipeline 接口,包含图形管线和计算管线。PSO 在创建时指定所有状态和着色器,之后可以快速切换。

cpp 复制代码
// Vulkan: 创建图形 PSO
VkGraphicsPipelineCreateInfo pipelineInfo = {};
pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pipelineInfo.stageCount = 2;
pipelineInfo.pStages = shaderStages;
pipelineInfo.pVertexInputState = &vertexInputInfo;
pipelineInfo.pInputAssemblyState = &inputAssemblyInfo;
pipelineInfo.pViewportState = &viewportInfo;
pipelineInfo.pRasterizationState = &rasterizerInfo;
pipelineInfo.pMultisampleState = &multisamplingInfo;
pipelineInfo.pDepthStencilState = &depthStencilInfo;
pipelineInfo.pColorBlendState = &colorBlendingInfo;
pipelineInfo.layout = pipelineLayout;
pipelineInfo.renderPass = renderPass;

VkPipeline graphicsPipeline;
vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &graphicsPipeline);

// 绑定 PSO
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);

7.3 着色器语言与编译

  着色器语言和编译模型是 API 差异的重要方面。不同 API 使用不同的着色器语言和编译流程。

API 着色器语言 编译格式 编译时机 跨平台支持
OpenGL GLSL GLSL 源码 运行时编译 有限(需驱动支持)
OpenGL ES ESSL ESSL 源码 运行时编译 有限(需驱动支持)
Direct3D 11 HLSL DXBC(字节码) 离线编译 仅 Windows/Xbox
Direct3D 12 HLSL DXIL(LLVM IR) 离线编译 仅 Windows/Xbox
Metal MSL Metal Library(LLVM IR) 离线编译 仅 Apple 平台
Vulkan GLSL/HLSL SPIR-V(二进制) 离线编译 跨平台
7.3.1 SPIR-V 的跨平台优势

  SPIR-V 是 Vulkan 的标准着色器格式,也是 OpenGL 的可选格式。它是一种紧凑的二进制格式,可以离线编译,避免运行时编译的开销和兼容性问题。

cpp 复制代码
// Vulkan: 加载 SPIR-V 着色器
std::vector<char> vertexShaderCode = ReadFile("vertex.spv");
VkShaderModuleCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
createInfo.codeSize = vertexShaderCode.size();
createInfo.pCode = reinterpret_cast<const uint32_t*>(vertexShaderCode.data());

VkShaderModule vertexShaderModule;
vkCreateShaderModule(device, &createInfo, nullptr, &vertexShaderModule);
7.3.2 着色器交叉编译

  跨平台 RHI 通常需要支持多种着色器语言。工具链如 SPIRV-Cross 可以将 SPIR-V 转换为 HLSL、MSL 或 GLSL,实现一次编写多平台编译。

cpp 复制代码
// SPIRV-Cross: 着色器交叉编译
spirv_cross::CompilerGLSL glsl(std::move(spirvBinary));
spirv_cross::CompilerGLSL::Options options;
options.version = 330;
options.es = false;
glsl.set_common_options(options);

std::string glslSource = glsl.compile();

7.4 绑定模型

  绑定模型决定了着色器如何访问资源(缓冲区、纹理、采样器)。不同 API 提供了不同粒度的绑定模型。

7.4.1 OpenGL 的全局绑定

  OpenGL 使用全局状态机,资源绑定通过全局函数调用。每次绘制前需要重新绑定所有资源,导致开销。

cpp 复制代码
// OpenGL: 全局绑定
glUseProgram(shaderProgram);
glBindVertexArray(vao);
glBindTexture(GL_TEXTURE_2D, texture);
glDrawArrays(GL_TRIANGLES, 0, 3);
7.4.2 Direct3D 11 的常量缓冲区

  DX11 使用常量缓冲区(Constant Buffer)绑定资源,支持按槽位绑定顶点着色器、像素着色器等。

cpp 复制代码
// DX11: 常量缓冲区绑定
ID3D11Buffer* constantBuffers[] = { constantBuffer };
deviceContext->VSSetConstantBuffers(0, 1, constantBuffers);
deviceContext->PSSetShaderResources(0, 1, &shaderResourceView);
deviceContext->PSSetSamplers(0, 1, &samplerState);
7.4.3 Direct3D 12 的根签名

  DX12 使用根签名(Root Signature)定义管线使用的资源布局。根签名包含根参数(根描述符、根常量、描述符表),描述符堆包含实际的描述符。

cpp 复制代码
// DX12: 根签名绑定
// 创建根签名
CD3DX12_ROOT_PARAMETER1 rootParameters[1];
rootParameters[0].InitAsDescriptorTable(1, &descriptorRange, D3D12_SHADER_VISIBILITY_ALL);

ComPtr<ID3D12RootSignature> rootSignature;
// ... 创建根签名

// 绑定描述符表
commandList->SetGraphicsRootDescriptorTable(0, descriptorHeap->GetGPUDescriptorHandleForHeapStart());
7.4.4 Metal 的参数缓冲区

  Metal 使用参数缓冲区(Argument Buffer)绑定资源,支持在单个缓冲区中打包多个资源引用。

swift 复制代码
// Metal: 参数缓冲区绑定
let argumentEncoder = device.makeArgumentEncoder(descriptor: argumentDescriptor)
argumentEncoder.setArgumentBuffer(argumentBuffer, offset: 0)
argumentEncoder.setTexture(texture, at: 0)
argumentEncoder.setSamplerState(samplerState, at: 1)

renderEncoder.setVertexBuffer(argumentBuffer, offset: 0, at: 0)
renderEncoder.setFragmentBuffer(argumentBuffer, offset: 0, at: 0)
7.4.5 Vulkan 的描述符集

  Vulkan 使用描述符集(Descriptor Set)绑定资源,描述符集布局定义资源类型和数量,描述符池分配描述符集。

cpp 复制代码
// Vulkan: 描述符集绑定
// 创建描述符集布局
VkDescriptorSetLayoutBinding binding = {};
binding.binding = 0;
binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
binding.descriptorCount = 1;
binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;

VkDescriptorSetLayoutCreateInfo layoutInfo = {};
layoutInfo.bindingCount = 1;
layoutInfo.pBindings = &binding;

VkDescriptorSetLayout descriptorSetLayout;
vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout);

// 分配描述符集
VkDescriptorSetAllocateInfo allocInfo = {};
allocInfo.descriptorPool = descriptorPool;
allocInfo.descriptorSetCount = 1;
allocInfo.pSetLayouts = &descriptorSetLayout;

VkDescriptorSet descriptorSet;
vkAllocateDescriptorSets(device, &allocInfo, &descriptorSet);

// 更新描述符集
VkDescriptorImageInfo imageInfo = {};
imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
imageInfo.imageView = textureView;
imageInfo.sampler = sampler;

VkWriteDescriptorSet descriptorWrite = {};
descriptorWrite.dstSet = descriptorSet;
descriptorWrite.dstBinding = 0;
descriptorWrite.descriptorCount = 1;
descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
descriptorWrite.pImageInfo = &imageInfo;

vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);

// 绑定描述符集
vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
    pipelineLayout, 0, 1, &descriptorSet, 0, nullptr);

7.5 跨平台 RHI 的管线状态策略

  设计跨平台 RHI 时,管线状态抽象需要平衡各后端的差异。常见的策略包括:

7.5.1 统一 PSO 接口

  RHI 可以定义统一的 PSO 接口,映射到各后端的具体实现:

cpp 复制代码
class RHIPipelineState {
public:
    virtual void Bind(RHICommandList& cmdList) = 0;
};

class RHIGraphicsPipelineState : public RHIPipelineState {
public:
    // 图形管线状态
};

class RHIComputePipelineState : public RHIPipelineState {
public:
    // 计算管线状态
};
7.5.2 统一绑定接口

  RHI 可以定义统一的绑定接口,隐藏各后端的绑定模型差异:

cpp 复制代码
class RHIDescriptorSet {
public:
    virtual void SetBuffer(RHIBuffer* buffer, uint32_t binding) = 0;
    virtual void SetTexture(RHITexture* texture, uint32_t binding) = 0;
    virtual void SetSampler(RHISampler* sampler, uint32_t binding) = 0;
    virtual void Bind(RHICommandList& cmdList, RHIPipelineStage stage) = 0;
};
7.5.3 着色器交叉编译

  RHI 可以集成 SPIRV-Cross 等工具,实现 SPIR-V 到各后端着色器语言的自动转换:

cpp 复制代码
class RHIShaderCompiler {
public:
    static RHIShader Compile(const std::string& source, RHIShaderStage stage);
    static RHIShader CrossCompile(const std::string& spirvSource, RHIPlatform platform);
};

7.6 小结

  管线状态与着色器架构是图形 API 的核心部分,决定了渲染管线的配置和着色器的执行方式。OpenGL 和 DX11 使用全局状态机,而 DX12、Metal 和 Vulkan 采用不可变的 PSO 模型。RHI 设计的关键是在这些差异之上建立统一的管线状态抽象,同时保留各后端的优化路径。统一 PSO 接口、统一绑定接口和着色器交叉编译是工程实践中的标准解决方案。

8 渲染目标与暂存区管理

  渲染目标与暂存区管理是图形 API 中处理帧缓冲、渲染通道和交换链的关键部分。旧一代 API 使用隐式的帧缓冲管理,而新一代 API 则提供了显式的渲染通道和附件管理。这种转变提高了性能,但也增加了复杂度。

8.1 渲染目标模型对比

API 渲染目标模型 渲染通道支持 交换链管理 加载/存储操作
OpenGL 帧缓冲对象(FBO) 隐式(通过 FBO) 驱动管理 隐式
Direct3D 11 渲染目标视图(RTV) 隐式(通过 OM 阶段) 驱动管理 隐式
Direct3D 12 渲染目标描述符 显式渲染通道 应用管理 显式
Metal 渲染通道描述符 显式渲染通道 应用管理 显式
Vulkan 帧缓冲附件 显式渲染通道 应用管理 显式

8.2 帧缓冲与渲染通道

8.2.1 OpenGL 的帧缓冲对象

  OpenGL 使用帧缓冲对象(Frame Buffer Object, FBO)管理渲染目标。FBO 可以附加颜色、深度和模板附件,支持渲染到纹理。

cpp 复制代码
// OpenGL: 帧缓冲对象
GLuint fbo;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);

// 附加颜色附件
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);

// 附加深度附件
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer);

// 检查完整性
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
    // 处理错误
}
8.2.2 Direct3D 12 的渲染通道

  DX12 使用显式渲染通道(Render Pass)管理渲染目标。渲染通道定义附件的加载/存储操作,以及子通道之间的依赖关系。

cpp 复制代码
// DX12: 渲染通道(通过根签名和管线状态隐式定义)
// DX12 通过 BeginRenderPass/EndRenderPass 提供显式渲染通道,用于声明附件的加载/存储操作和优化 tile 内存使用。
D3D12_RENDER_PASS_RENDER_TARGET_DESC renderTarget = {};
renderTarget.cpuDescriptor = rtvHandle;
renderTarget.BeginningAccess.Type = D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_CLEAR;
renderTarget.BeginningAccess.Clear.ClearValue.Color[0] = 0.0f;
renderTarget.EndingAccess.Type = D3D12_RENDER_PASS_ENDING_ACCESS_TYPE_PRESERVE;

commandList->BeginRenderPass(1, &renderTarget, nullptr, D3D12_RENDER_PASS_FLAG_NONE);
// ... 渲染命令
commandList->EndRenderPass();
8.2.3 Metal 的渲染通道

  Metal 使用显式渲染通道描述符(Render Pass Descriptor)管理渲染目标。渲染通道描述符定义附件的加载/存储操作和存储模式。

swift 复制代码
// Metal: 渲染通道描述符
let renderPassDescriptor = MTLRenderPassDescriptor()
renderPassDescriptor.colorAttachments[0].texture = renderTargetTexture
renderPassDescriptor.colorAttachments[0].loadAction = .clear
renderPassDescriptor.colorAttachments[0].storeAction = .store
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 1)
renderPassDescriptor.depthAttachment.texture = depthTexture
renderPassDescriptor.depthAttachment.loadAction = .clear
renderPassDescriptor.depthAttachment.storeAction = .dontCare

let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)!
// ... 渲染命令
renderEncoder.endEncoding()
8.2.4 Vulkan 的帧缓冲与渲染通道

  Vulkan 使用显式帧缓冲(Framebuffer)和渲染通道(Render Pass)管理渲染目标。渲染通道定义附件的加载/存储操作和子通道依赖,帧缓冲绑定实际的附件图像。

cpp 复制代码
// Vulkan: 渲染通道与帧缓冲
// 创建渲染通道
VkAttachmentDescription colorAttachment = {};
colorAttachment.sType = VK_STRUCTURE_TYPE_ATTACHMENT_DESCRIPTION;
colorAttachment.format = VK_FORMAT_B8G8R8A8_UNORM;
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;

VkAttachmentReference colorAttachmentRef = {};
colorAttachmentRef.attachment = 0;
colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

VkSubpassDescription subpass = {};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorAttachmentRef;

VkRenderPassCreateInfo renderPassInfo = {};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
renderPassInfo.attachmentCount = 1;
renderPassInfo.pAttachments = &colorAttachment;
renderPassInfo.subpassCount = 1;
renderPassInfo.pSubpasses = &subpass;

VkRenderPass renderPass;
vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass);

// 创建帧缓冲
VkFramebufferCreateInfo framebufferInfo = {};
framebufferInfo.renderPass = renderPass;
framebufferInfo.attachmentCount = 1;
framebufferInfo.pAttachments = &swapchainImageView;
framebufferInfo.width = swapchainExtent.width;
framebufferInfo.height = swapchainExtent.height;
framebufferInfo.layers = 1;

VkFramebuffer framebuffer;
vkCreateFramebuffer(device, &framebufferInfo, nullptr, &framebuffer);

// 渲染通道内渲染
VkClearValue clearColor = {{{0.0f, 0.0f, 0.0f, 1.0f}}};
VkRenderPassBeginInfo renderPassBeginInfo = {};
renderPassBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassBeginInfo.renderPass = renderPass;
renderPassBeginInfo.framebuffer = framebuffer;
renderPassBeginInfo.renderArea.extent = swapchainExtent;
renderPassBeginInfo.clearValueCount = 1;
renderPassBeginInfo.pClearValues = &clearColor;

vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
// ... 渲染命令
vkCmdEndRenderPass(commandBuffer);

8.3 交换链管理

  交换链(Swap Chain)管理屏幕呈现,协调 CPU 和 GPU 的帧同步。不同 API 提供了不同粒度的交换链控制。

API 交换链管理 呈现模式 垂直同步控制
OpenGL 驱动管理(glfwSwapBuffers 隐式双缓冲 驱动控制
Direct3D 11 驱动管理(IDXGISwapChain 显式双缓冲 驱动控制
Direct3D 12 应用管理(IDXGISwapChain3 显式多缓冲 应用控制
Metal 应用管理(CAMetalLayer 显式三缓冲 应用控制
Vulkan 应用管理(VkSwapchainKHR 显式多缓冲 应用控制
8.3.1 Vulkan 的交换链

  Vulkan 的交换链管理最为复杂,需要应用显式创建交换链、获取图像、管理呈现同步。

cpp 复制代码
// Vulkan: 交换链管理
// 创建交换链
VkSwapchainCreateInfoKHR swapchainInfo = {};
swapchainInfo.surface = surface;
swapchainInfo.minImageCount = 3;
swapchainInfo.imageFormat = surfaceFormat.format;
swapchainInfo.imageExtent = extent;
swapchainInfo.imageArrayLayers = 1;
swapchainInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
swapchainInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
swapchainInfo.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
swapchainInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
swapchainInfo.presentMode = VK_PRESENT_MODE_FIFO_KHR;

VkSwapchainKHR swapchain;
vkCreateSwapchainKHR(device, &swapchainInfo, nullptr, &swapchain);

// 获取交换链图像
std::vector<VkImage> swapchainImages;
vkGetSwapchainImagesKHR(device, swapchain, &imageCount, swapchainImages.data());

// 呈现图像
uint32_t imageIndex;
vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex);

VkPresentInfoKHR presentInfo = {};
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = &swapchain;
presentInfo.pImageIndices = &imageIndex;
presentInfo.pWaitSemaphores = &renderFinishedSemaphore;

vkQueuePresentKHR(presentQueue, &presentInfo);

8.4 暂存区与加载/存储操作

  暂存区(Staging)管理渲染目标的加载(Load)和存储(Store)操作,决定附件内容在渲染通道前后的处理方式。

API 加载操作 存储操作 暂存策略
OpenGL 隐式(glClear 隐式(自动保存) 驱动管理
Direct3D 11 隐式(ClearRenderTargetView 隐式(自动保存) 驱动管理
Direct3D 12 显式(D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_* 显式(D3D12_RENDER_PASS_ENDING_ACCESS_TYPE_* 应用控制
Metal 显式(MTLLoadAction* 显式(MTLStoreAction* 应用控制
Vulkan 显式(VK_ATTACHMENT_LOAD_OP_* 显式(VK_ATTACHMENT_STORE_OP_* 应用控制
8.4.1 优化加载/存储操作

  显式加载/存储操作允许应用优化渲染目标的内存访问。常见的优化策略包括:

  • 清除(Clear):用固定值填充附件,丢弃原有内容。
  • 保持(Preserve):保留附件原有内容,用于后续渲染通道。
  • 放弃(Don't Care):不关心附件原有内容,通常用于临时渲染目标。
  • 存储(Store):保存渲染结果,用于后续采样或呈现。

8.5 跨平台 RHI 的渲染目标策略

  设计跨平台 RHI 时,渲染目标抽象需要平衡各后端的差异。常见的策略包括:

8.5.1 统一渲染通道接口

  RHI 可以定义统一的渲染通道接口,映射到各后端的具体实现:

cpp 复制代码
class RHIRenderPass {
public:
    virtual void Begin(RHICommandList& cmdList) = 0;
    virtual void End(RHICommandList& cmdList) = 0;
    virtual void SetClearColor(float r, float g, float b, float a) = 0;
};
8.5.2 统一帧缓冲接口

  RHI 可以定义统一的帧缓冲接口,隐藏各后端的附件管理差异:

cpp 复制代码
class RHIFramebuffer {
public:
    virtual void AttachColor(uint32_t index, RHITexture* texture) = 0;
    virtual void AttachDepth(RHITexture* texture) = 0;
    virtual void Validate() = 0;
};
8.5.3 统一交换链接口

  RHI 可以定义统一的交换链接口,管理屏幕呈现:

cpp 复制代码
class RHISwapChain {
public:
    virtual RHITexture* GetCurrentImage() = 0;
    virtual void Present() = 0;
    virtual void Resize(uint32_t width, uint32_t height) = 0;
};

8.6 小结

  渲染目标与暂存区管理是图形 API 中处理帧缓冲、渲染通道和交换链的关键部分。OpenGL 和 DX11 使用隐式管理,而 DX12、Metal 和 Vulkan 提供显式控制。RHI 设计的关键是在这些差异之上建立统一的渲染目标抽象,同时保留各后端的优化路径。统一渲染通道接口、统一帧缓冲接口和统一交换链接口是工程实践中的标准解决方案。

9 小结

  本文系统对比了六大主流图形 API(OpenGL、OpenGL ES、Direct3D 11、Direct3D 12、Metal、Vulkan)在资源管理、多线程、同步机制、管线状态、着色器架构、渲染目标等多个维度的差异,并分析了这些差异背后的硬件与历史考量。

9.1 核心差异总结

  六大 API 的差异可以压缩成一句话:责任在谁手里。GL/GLES/DX11 把资源、同步、内存交给驱动,换来简单;DX12/Metal/Vulkan 把这些责任交还给应用,换来性能与可控。

维度 驱动托管 API(OpenGL/DX11) 显式控制 API(DX12/Metal/Vulkan)
资源管理 驱动分配、驻留、回收 应用创建堆、管理内存、控制驻留
多线程 有限或不支持 原生多线程命令录制
同步 隐式同步 显式栅栏、信号量、屏障
管线状态 全局状态机,高切换开销 不可变 PSO,低切换开销
着色器 运行时编译,跨平台有限 离线编译,SPIR-V 跨平台
渲染目标 隐式帧缓冲管理 显式渲染通道和附件管理

9.2 RHI 设计的关键原则

  设计跨平台 RHI 时,需要遵循以下关键原则:

  1. 接口形状像最显式的 API:以 Vulkan/DX12 为基准设计接口,然后向旧 API 降级仿真。
  2. 能力分级:根据后端能力提供不同级别的功能,确保旧硬件也能运行。
  3. 统一绑定模型:抽象描述符集、根签名、参数缓冲区等差异,提供统一的资源绑定接口。
  4. 显式同步抽象:隐藏栅栏、信号量、屏障的差异,提供统一的同步原语。
  5. 着色器交叉编译:集成 SPIRV-Cross 等工具,实现 SPIR-V 到各后端着色器语言的自动转换。
  6. 零开销热路径:确保关键渲染路径没有额外抽象开销。
  7. 跨后端测试:建立完整的测试套件,确保各后端行为一致。

9.3 工程实践建议

  在实际工程中,需要根据自己的场景考虑如何实现RHI,比如对于高性能场景可能薄薄的封装一层比较合适,而对于需要多平台支持的场景略微牺牲性能所有render API都对上层透明可能更好:

  • 内存管理:使用子分配器(如 VMA、D3D12MA)减少显存分配次数,支持延迟删除和资源别名。
  • 多线程架构:采用线程本地存储和任务队列,平衡各后端的线程模型差异。
  • 同步策略:实现自动状态转换和延迟同步,减少显式同步的复杂度。
  • 管线状态缓存:缓存编译后的 PSO,避免重复创建和编译开销。
  • 渲染通道抽象:统一渲染通道和帧缓冲接口,支持移动端的 TBDR 优化。

9.4 结语

  图形 API 的差异不是随意演化的,而是历史包袱、目标硬件(IMR vs TBDR、统一内存 vs 离散显存)与成本模型共同作用的产物。也正因为如此,设计 RHI 的第一原则才显得顺理成章:接口的形状应该像最显式的那个 API,然后向老 API 降级仿真------再加上能力分级、统一绑定、显式同步、着色器交叉编译这几根支柱,以及零开销热路径和跨后端测试这两条纪律。做到这些,一套代码跑遍六家后端就不再是玄学,而是工程。

  理解这些差异不仅是为了写一个跨平台渲染器,更是为了理解 GPU 的工作方式本身。当你深入理解了这些 API 的设计哲学,你就能更好地利用硬件能力,创造出更高效、更精美的图形应用。

相关推荐
热心网友俣先生1 小时前
2026国赛C题:五年命题规律与预测
java·c语言·前端·数学建模
Titan20241 小时前
网络基础:传输层协议知识梳理
linux·服务器·网络
爱吃提升2 小时前
VMware虚拟机迁移系统磁盘完整教程(VMware Workstation17)
linux·运维·服务器
诸葛李2 小时前
linux 配置
linux·运维·服务器
TechWayfarer2 小时前
做IP归属地查询时总是查不准?IP纯净度检测实战:从“查归属地“到识别“脏IP“的四个维度
服务器·网络·python·tcp/ip·安全·web安全
Aime_Perfect2 小时前
sqlserver always on
服务器·数据库·sqlserver
BestHeaker3 小时前
跨企业接口对接:IQDS / CPK 数据解析与协作避坑指南(五)
java·服务器·前端
其实防守也摸鱼3 小时前
OpenClaw 深度解析:从原理到实战的完整指南
运维·服务器·数据库·github·copilot
青花锁3 小时前
OpenAI Function Calling 完全指南:让大模型安全调用你的业务系统
前端·安全·functioncalling