GRAPHICS × PRODUCTION
中EN

皮了个牛

↓

计算机图形学

Infinity Browser中的GPU HAL

系统梳理 GPU HAL 的设计:比较 Bgfx、NVRHI、UE4 RHI、Unity Gfx、WebGPU 与 The Forge,随后定义 Instance、Device、能力模型、资源与视图、内存驻留、管线、绑定、命令编码、队列同步、查询、加速结构、交换链及高级特性,最后串起启动渲染流程。

2022-08-19100 分钟阅读
Infinity Browser中的GPU HAL
AI 生成概念封面

GPU 平台 API 抽象是渲染架构中长期存在的核心问题。复杂度来源于多个维度:光栅化管线本身的状态组合爆炸,Compute 管线与 IO 管线的异构调度,不同硬件架构(IMR、TBDR)的行为差异,以及各平台功能集的不对称。在规划自己的 GPU 抽象(SharpGPU)之前,我对几个主流开源方案做了调研和实际使用。

一、现有方案的问题

1.1 Bgfx

设计思想偏向状态机模型,将现代图形 API 的显式控制退化为传统 API 的隐式语义,执行效率受限。全平台覆盖是优点,但结构过度面向渲染引擎场景,对大规模应用的运行时优化空间有限。

1.2 NVIDIA RHI (NVRHI)

商业化导向明显,开启了大量 NVIDIA 独占扩展,在 AMD 硬件上兼容性存在问题(6900XT 上无法正常运行)。目标平台为 Windows 和 Linux,通过 MoltenVK 可间接支持 macOS。结构偏 High-Level,Device 中耦合了 PerFrame 相关的操作逻辑。

1.3 UE4 RHI

与引擎深度融合,为兼容传统 API,CommandList 执行分为 ByPass 和 Immediate 两种模式。ByPass 模式通过缓存命令给 RHI 线程来缓解 CPU 同步开销,但后端并行翻译会与 Game/Physics 等功能线程争抢 TaskSystem 资源,为 CPU 延迟埋下隐患。CommandBuffer 相关抽象包含 FRHICommandListBase、FRHICommandList、FRHICommandListImmediate、IRHIComputeContext、IRHICommandContext、FDynamicRHI、FRHICommandListExecutor 等多个类型,彼此交叉引用,结构冗余。

1.4 Unity Gfx

大量隐式操作(Resource Barrier、State Cache),为后续性能优化增加了工作量。上层仅暴露 CommandBuffer 和 ScriptableRenderContext,可控粒度有限。部分 Renderer 层面的逻辑(如 Batch)下沉到抽象层,简化了使用但引入强耦合。为兼容传统 API 增加了中间层,但由于缺少并行翻译,延迟提交失去了原有的异步优势。

1.5 WebGPU

结构最清晰的 Graphics API 抽象,对象模型干净。代价是功能受限:无显式 Barrier、多 Device 与多 Queue 支持不足、扩展开放度低,自定义空间有限。Shader 语言强制绑定了 WGSL,而非直接接受 SPIR-V 或 DXIL,对已有 Shader 资产不够友好。

1.6 The Forge

全平台覆盖,API 设计本身还不错。问题在于实现层面没有真正抹平平台差异:不同平台通过宏开关直接暴露了大量 native graphics API 的参数,抽象层该屏蔽的东西漏了出来。构建复杂度和耦合度也偏高。


二、设计目标

结合上述方案的优缺点,SharpGPU 的抽象层设计遵循以下原则:

提供机制而非策略(Mechanisms, not policies)。 硬件抽象层只负责提供正交、确定、能够无损穿透映射至现代图形 API 的底层操作原语,清除驱动黑盒与隐式状态跟踪。Pass 拓扑编排、全局屏障推导、Memory Aliasing 内存复用以及瞬态资源的生命周期管理,交由上层的 RenderGraph 编译器在更高维度上消化。

剥离陈旧代偿,显式表达物理意图。 剔除传统 API 遗留的包袱(如无物理数据搬运的虚假 Layout 转换、状态组合爆炸的巨型 PSO),但保留硬件优化所必需的结构信息,包括显式执行域(Execution Domains)、拓扑元数据与准确的内存访问契约。

对称支持异构执行范式。 抽象层保持通道的正交性,既要为算力受限平台的传统多线程 CPU 录制提供低延迟、零竞争的命令流,也要能直通现代高端硬件的纯 GPU-driven 间接执行与片内 Work Graphs 派发。

三、Instance 与 Device

后端选择与 Instance 创建

SharpGPU用一个枚举表示图形API后端:

public enum ERHIBackend
{
    Metal,
    Vulkan,
    DirectX12,
    Pending
}

Pending表示尚未决定,创建Instance时仍然处于Pending状态会直接抛异常。这里没有"Auto"选项,自动选择通过一个独立的查询方法处理:

public static ERHIBackend GetBackendByPlatform(in bool bForceVulkan)

这个方法根据当前操作系统返回推荐后端。Windows上默认返回DirectX12,macOS返回Metal,Linux返回Vulkan。传入bForceVulkan为true可以在Windows上强制使用Vulkan,主要用于调试Vulkan后端。返回值只是建议,调用方自己填入Descriptor。

实际使用中还经常需要判断某个后端在当前环境下是否可用,例如Windows ARM64上的D3D12支持情况、Linux上Vulkan驱动是否就绪:

public static bool IsBackendSupported(ERHIBackend backend, out string reason)

这个方法检查运行时条件(动态库是否存在、驱动版本是否达标),返回false时通过reason给出具体原因。

DirectX12后端还受编译开关控制。在不需要D3D12的平台上(macOS、Linux),通过SHARPGPU_ENABLE_DX12条件编译完全剥离D3D12代码路径,避免引入不必要的依赖:

#if SHARPGPU_ENABLE_DX12
    if (descriptor.Backend == ERHIBackend.DirectX12)
        return new D3D12Instance(descriptor);
#endif

Instance创建时需要声明队列需求。队列数量直接作为RHIInstanceDescriptor的字段,零或负数表示不需要该类型的队列:

public struct RHIInstanceDescriptor
{
    // ... 其他字段
    public int GraphicsQueueRequestCount;
    public int ComputeQueueRequestCount;
    public int TransferQueueRequestCount;
}

Device 状态与生命周期

Instance持有所有Device的所有权,上层通过索引访问:

public int DeviceCount { get; }
public RHIDevice GetDevice(int index);

单GPU系统下DeviceCount为1。多GPU配置(笔记本的集显加独显)下,每个物理适配器对应一个Device。

Device在运行时可能经历多种状态变化:

public enum ERHIDeviceState
{
    Unknown,
    Operational,
    Lost,
    Removed,
    Reset
}

Unknown是初始状态,正常创建完成后进入Operational。Lost通常意味着GPU挂了(TDR超时),驱动正在尝试恢复。Removed表示物理设备被移除(eGPU拔线、驱动更新卸载了设备)。Reset表示设备经历了完整的重置流程,之前所有的资源和状态都已失效,需要全部重建。

跨Device隔离是运行时强制的。每个资源对象内部持有OwnerDevice引用,使用时做匹配检查:

internal void ValidateOwnership(RHIDevice expectedDevice)
{
    if (OwnerDevice != expectedDevice)
        throw new RHIDeviceMismatchException(OwnerDevice, expectedDevice);
}

Device 工厂方法

Device是SharpGPU中最大的工厂对象,提供40多个创建和查询方法。按功能分组:

资源创建: CreateBuffer、CreateTexture、CreateSampler、CreateFence、CreateSwapChain、CreateHeap、CreateTensor、CreateFunctionLibrary等。这些是基础的Committed资源创建,内存由驱动自动分配。

管线创建: CreateRasterPipeline、CreateComputePipeline、CreateRayTracingPipeline、CreatePipelineLayout等。管线对象是不可变的,创建后不能修改。

可选资源创建: CreatePlacedBuffer、CreatePlacedTexture、CreateSparseTexture等。Placed、Sparse 等分配模式是可选能力,需要先查询 Capabilities 确认硬件支持再调用。

设备级能力查询: GetCapabilities返回整个设备的能力描述,包括各功能域的Tier等级。GetDeviceLimit返回硬件极限值。这些信息在Device创建后不变。

资源级支持查询: QueryFormatSupport、QueryMSAASupport、QuerySamplerFeedbackSupport等。这些方法接受具体的资源参数(格式、采样数),返回该组合是否被支持。区别在于:Capabilities描述设备整体能做什么,Query*Support判断一个具体的资源配置是否可行。

调试工具: TryToggleGpuCapture用于在运行时触发GPU帧捕获(PIX、RenderDoc、Xcode GPU Capture),返回值标示是否成功。SetDebugName给资源设置调试名称。

能力模型

SharpGPU的能力查询按14个功能域组织。每个域对应RHIDeviceCapabilities上的一个属性,类型为独立的Capabilities类:

public class RHIDeviceCapabilities
{
    public RHIRasterCapabilities Raster { get; }
    public RHIBindingCapabilities Binding { get; }
    public RHISynchronizationCapabilities Synchronization { get; }
    public RHIMemoryCapabilities Memory { get; }
    public RHIStorageCapabilities Storage { get; }
    public RHIPipelineCacheCapabilities PipelineCache { get; }
    public RHIPresentationCapabilities Presentation { get; }
    public RHIRayTracingCapabilities RayTracing { get; }
    public RHIMeshCapabilities Mesh { get; }
    public RHIMachineLearningCapabilities MachineLearning { get; }
    public RHIWorkGraphCapabilities WorkGraph { get; }
    public RHIIndirectCommandBufferCapabilities IndirectCommandBuffer { get; }
    public RHIComputeCapabilities Compute { get; }
    public RHIFunctionLibraryCapabilities FunctionLibrary { get; }
}

用14个独立的类而非一个枚举,是因为每个域的内部结构差异很大。RayTracing域需要描述加速结构层级支持、Ray Query可用性、Motion Blur等;Mesh域需要描述Amplification/Task Shader支持和Meshlet参数上限;MachineLearning域关注Tensor Core可用性和推理精度。这些信息很难塞进一个统一的枚举加Tier的扁平模型。每个Capabilities类内部自行定义属性和Tier粒度。

每个域的支持程度用Tier表示:

public enum ERHICapabilityTier
{
    Unavailable,
    Tier1,
    Tier2,
    Tier3,
    Tier4
}

Unavailable表示完全不支持。Tier1到Tier4表示递增的功能等级。以RayTracing为例,Tier1可能只支持Ray Query(在Compute或Pixel Shader中做inline ray tracing),Tier2支持完整的Ray Tracing Pipeline,Tier3支持硬件加速的Motion Blur,Tier4对应最新的GPU架构特性。每个域的Tier含义在文档中有完整定义。

能力的来源也会被追踪:

public readonly struct RHICapabilityProvenance
{
    public readonly ERHICapabilityProbeKind Kind;
    public readonly string Source;
}

public enum ERHICapabilityProbeKind
{
    BackendContract,
    ApiVersion,
    NativeFeatureQuery,
    NativeExtensionQuery,
    RuntimeObjectProbe,
    RuntimeLibraryProbe
}

BackendContract表示后端规范保证的能力,最可靠。ApiVersion表示根据API版本确定。NativeFeatureQuery表示通过原生特性查询接口得到。NativeExtensionQuery表示通过扩展可用性查询得到。RuntimeObjectProbe表示通过尝试创建运行时对象来探测。RuntimeLibraryProbe表示通过检查运行时库是否存在来探测。调试时打印Provenance可以快速定位"为什么这张卡上RayTracing Tier只有1"之类的问题。

ERHICapabilityStrategy描述能力的实现策略,即某个功能在当前后端上如何落地:

public enum ERHICapabilityStrategy
{
    Unavailable,
    CoreApi,
    NativeExtension,
    NativeSpecialized,
    NativeLibrary
}

Unavailable表示没有可用的实现路径。CoreApi表示由图形API核心规范直接提供。NativeExtension表示通过平台扩展实现(如Vulkan扩展)。NativeSpecialized表示通过平台特化的专有接口实现。NativeLibrary表示依赖外部原生库提供。上层可以根据Strategy判断功能的可靠性和性能预期:CoreApi路径通常最稳定,NativeLibrary路径可能有额外的加载开销和兼容性风险。

除了Tier之外,大量离散的数值型限制通过RHICapabilityLimits查询:

public class RHICapabilityLimits
{
    public bool TryGetValue(ERHICapabilityLimitKind kind, out ulong value);
}

ERHICapabilityLimitKind枚举包含约60种限制类型,涵盖最大纹理尺寸、最大Buffer大小、最大Compute线程组维度等。用TryGetValue是因为不同后端和硬件世代暴露的限制集合不同,查不到说明该后端不报告这个值,上层应该用保守默认值。返回类型是ulong,足以容纳所有平台上的极限值。

设备限制

高频使用的硬件限制单独放在RHIDeviceLimit中,避免每次都走TryGetValue的字典查询路径:

public class RHIDeviceLimit
{
    public readonly int UniformBufferAlignment;
    public readonly int UploadBufferAlignment;
    public readonly int UploadBufferTextureAlignment;
    public readonly int UploadBufferTextureRowAlignment;
    public readonly int MaxMSAACount;
    public readonly int MaxBoundTexture;
    public readonly int MinWavefrontSize;
    public readonly int MaxWavefrontSize;
    public readonly int MaxComputeThreads;
    public readonly int MaxGroupShareMemorySize;
    public readonly int MaxVertexInputBindings;
    public readonly int MaxColorAttachments;
    public readonly int MaxTexture2DSize;
    public readonly int MaxTextureCubeSize;
}

这 14 个字段覆盖了日常渲染最常碰到的硬件约束。对齐值(前四个)在创建Upload Buffer和做纹理上传时需要反复用到。MinWavefrontSize和MaxWavefrontSize反映GPU的Wave/Warp宽度范围:AMD RDNA架构支持Wave32和Wave64两种模式,这两个字段分别是32和64;Nvidia GPU两个值都是32。Shader里用Wave Intrinsics做Subgroup操作时,必须知道这个范围才能正确设置线程组大小。

厂商与适配器标识

SharpGPU对GPU厂商有完整的识别体系:

public readonly struct RHIVendorId
{
    public readonly uint Value;
}

public readonly struct RHIDeviceId
{
    public readonly uint Value;
}

public enum ERHIVendorType : uint
{
    AMD         = 0x1002,
    Nvidia      = 0x10DE,
    Intel       = 0x8086,
    Apple       = 0x106B,
    Mali        = 0x13B5,
    Adreno      = 0x5143,
    Vivante     = 0x7a05,
    Broadcom    = 0x14E4,
    Microsoft   = 0x1414,
    SamsungAMD  = 0x144D,
    Imagination = 0x1010,
    Kazan       = 0x10003,
    Codeplay    = 0x10004,
    Mesa        = 0x10005
}

PCI Vendor ID是标准值(AMD 0x1002、Nvidia 0x10DE、Intel 0x8086),移动GPU从ARM Mali到Qualcomm Adreno到三星定制的AMD IP都有覆盖。Vivante和Broadcom使用各自的PCI ID,Kazan、Codeplay、Mesa是软件实现或开源驱动的标识。识别厂商是为了应用厂商特定的workaround,因为不同驱动的Bug profile差异很大。

多GPU场景下需要精确匹配适配器:

public readonly struct RHIAdapterIdentity
{
    public readonly long Luid;
    public readonly Guid DeviceUuid;
    public bool HasLuid { get; }
    public bool HasDeviceUuid { get; }

    public void RequireMatch(in RHIAdapterIdentity expected, string operation);
}

Luid是Windows的Locally Unique Identifier,DeviceUuid是Vulkan的物理设备UUID。HasLuid和HasDeviceUuid标记对应标识符是否有效,因为不是所有平台都提供两种标识,Vulkan环境下可能只有DeviceUuid,Windows上通常两者都有。

RequireMatch是一个断言方法,验证当前Identity是否匹配expected。匹配失败时抛出异常,operation参数给出失败上下文(比如"SwapChain creation")。用于渲染输出必须发生在特定GPU上的场景,比如eGPU直连了显示器,渲染结果必须在那张卡上产生才能零拷贝Present。

四、资源对象与视图

Buffer 与 Texture

SharpGPU的GPU资源归结为两类基础对象:Buffer和Texture。

Buffer是线性的GPU内存块。创建时指定大小、格式和用途:

public struct RHIBufferDescriptor
{
    public int ByteSize;
    public ERHIBufferFormat Format;
    public ERHIBufferUsage UsageFlag;
    public ERHIStorageMode StorageMode;
}

ByteSize是Buffer的字节大小。ERHIBufferFormat描述Buffer中元素的格式:对于Raw Buffer是无类型的,对于Typed Buffer则指定具体的元素格式。ERHIBufferUsage是标志位组合,标记这块Buffer的可能用途,包括Vertex Buffer、Index Buffer、Constant Buffer、ShaderResource、UnorderedAccess、Indirect Argument等。StorageMode控制内存的物理位置和访问特性。

Texture比Buffer复杂,携带维度、格式、Mip链等信息:

public struct RHITextureDescriptor
{
    public uint MipCount;
    public uint3 Extent;
    public ERHIPixelFormat Format;
    public ERHISampleCount SampleCount;
    public ERHIStorageMode StorageMode;
    public ERHITextureUsage UsageFlag;
    public ERHITextureDimension Dimension;
}

Extent是uint3类型,三个分量分别对应宽、高和深度(或数组大小)。ERHITextureDimension区分Texture1D、Texture2D、Texture3D、TextureCube。对于Texture Array,Extent.z字段的含义取决于Dimension:3D纹理时是深度,其余情况是数组大小。MipCount指定Mip链层数。SampleCount是枚举类型,对应MSAA采样数。

ERHIStorageMode统一描述资源的内存位置策略,对应Metal的MTLStorageMode概念。具体的分配方式(Committed、Placed、Sparse)通过不同的Device创建方法区分:Committed走Device.CreateBuffer/CreateTexture,Placed走Device.CreatePlacedBuffer/CreatePlacedTexture,Sparse走Device.CreateSparseTexture。

View 系统

GPU资源和Shader看到的东西之间有一层间接,即View。一个4096×4096的RGBA8纹理是一个资源,但Shader可能只需要访问它的Mip 2到Mip 5、Array Slice 0到3这个子集,而且要把它当做SRV来读。View就是这个"从资源上切出一个子集并指定访问方式"的中间描述。

SharpGPU有四种View:

public struct RHIBufferViewDescriptor
{
    public int Count;
    public int Offset;
    public int Stride;
    public ERHIBufferViewType ViewType;
}

RHIBufferViewDescriptor描述Buffer中一段连续区域。Count是元素数量,Offset是起始偏移(字节),Stride是元素步长。ERHIBufferViewType包括ShaderResource、UnorderedAccess、Constant等。

public struct RHITextureViewDescriptor
{
    public uint MipCount;
    public uint BaseMipLevel;
    public uint ArrayCount;
    public uint BaseArraySlice;
    public ERHITextureViewType ViewType;
}

RHITextureViewDescriptor描述Texture的Mip和Array子集。BaseMipLevel和MipCount选中一段Mip范围,BaseArraySlice和ArrayCount选中一段Array范围。ViewType决定这个View作为SRV、UAV、RTV还是DSV使用。所有字段都是uint类型。

public struct RHITensorView
{
    public long Offset;
    public ReadOnlyMemory<int> Dimensions;
    public ReadOnlyMemory<int> Strides;
}

RHITensorView从RHITensor.CreateView创建,面向ML推理场景。Dimensions描述张量形状(比如[1, 3, 224, 224]),Strides描述每个维度的步长。GPU上的Tensor数据可以按ML框架期望的布局被Shader访问。

public struct RHIFunctionView
{
    public string EntryName;
    public ERHIFunctionType Type;
}

RHIFunctionView从RHIFunctionLibrary.CreateFunction创建。FunctionLibrary是D3D12 Work Graphs和Metal Function Tables引入的概念,将一组Shader函数编译到一个库里,运行时按名字和类型选择调用。EntryName是函数入口名,Type区分Vertex、Fragment、Compute等函数类型。

Sampler

Sampler描述纹理采样行为。SharpGPU的Sampler分为动态创建的和静态预定义的两类。

public struct RHISamplerDescriptor
{
    public float LodMin;
    public float LodMax;
    public float MipLODBias;
    public uint Anisotropy;
    public ERHIFilterMode MinFilter;
    public ERHIFilterMode MagFilter;
    public ERHIFilterMode MipFilter;
    public ERHIAddressMode AddressModeU;
    public ERHIAddressMode AddressModeV;
    public ERHIAddressMode AddressModeW;
    public ERHIComparisonMode ComparisonMode;
}

11个字段覆盖了三轴过滤方式、三轴寻址模式、比较模式(Shadow Map采样用)、各向异性等级和LOD控制。动态Sampler通过Device.CreateSampler创建,得到一个RHISampler对象。

对于在管线整个生命周期内不变的采样器,Static Sampler更高效,因为它们直接嵌入Pipeline Layout,不占用Descriptor Heap空间:

public struct RHIStaticSamplerElement
{
    public uint BindSlot;
    public RHISamplerDescriptor SamplerDescriptor;
}

public struct RHIStaticSamplerDescriptor
{
    public uint Index;
    public Memory<RHIStaticSamplerElement> Elements;
}

RHIStaticSamplerElement把采样器描述和它在Shader中的绑定槽位打包在一起。RHIStaticSamplerDescriptor是一组Static Sampler的集合,通过Index标识,创建Pipeline Layout时一并传入。

Sampler Feedback

Sampler Feedback是D3D12引入的特性,用于追踪Shader实际采样了纹理的哪些Mip Level或哪些区域。典型应用是Virtual Texture系统,通过Feedback数据决定哪些物理Page需要驻留。

public enum ERHISamplerFeedbackMode
{
    MinMip,
    MipRegionUsed
}

public struct RHISamplerFeedbackMapDescriptor
{
    public RHITexture? PairedTexture;
    public ERHISamplerFeedbackMode Mode;
    public uint3 MipRegion;
}

MinMip模式记录每个区域被采样的最小Mip Level。MipRegionUsed模式记录每个区域被访问的Mip范围。MipRegion是uint3类型,控制Feedback Map的粒度:值越小粒度越细,Feedback Map本身的内存开销越大。

Feedback Map必须在创建时就和目标纹理配对,这是关键约束。PairedTexture在Descriptor中指定,之后不能更改。Encoder(命令编码器)无法在录制时重新配对,因为Feedback Map的尺寸和格式取决于配对纹理的属性,必须在创建时确定。

[Flags]
public enum ERHISamplerFeedbackOperation
{
    None    = 0,
    Clear   = 1 << 0,
    Resolve = 1 << 1,
    Decode  = 1 << 2,
    Copy    = 1 << 3,
    All     = Clear | Resolve | Decode | Copy
}

ERHISamplerFeedbackOperation是标志位枚举,定义了对Feedback Map可执行的操作类型:Clear清除数据、Resolve合并采样结果、Decode回读Feedback数据、Copy拷贝Feedback内容。可以组合使用,All表示全部操作。

五、内存管理

Heap

D3D12、Vulkan、Metal都有显式内存管理的概念,只是术语不同:D3D12叫Heap,Vulkan叫DeviceMemory,Metal叫MTLHeap。SharpGPU统一用Heap:

public struct RHIHeapDescriptor
{
    public long SizeInBytes;
    public ERHIHeapType HeapType;
    public ERHIHeapFlags Flags;
}

ERHIHeapType对应内存的物理位置和访问特性:Default是GPU本地显存(最快但CPU不可访问),Upload是CPU可写的上传堆(CPU写入、GPU读取),Readback是CPU可读的回读堆(GPU写入、CPU读取)。

Heap创建后,通过CreatePlacedBuffer和CreatePlacedTexture在其中放置资源。这是手动内存管理的核心路径:多个资源可以共享一个Heap的不同区域,甚至在时间上互斥的资源可以alias同一块内存。

public RHIBuffer CreatePlacedBuffer(
    in RHIBufferDescriptor descriptor,
    RHIHeap heap,
    long heapOffset);

public RHITexture CreatePlacedTexture(
    in RHITextureDescriptor descriptor,
    RHIHeap heap,
    long heapOffset);

Placed 资源是可选能力,对应 Capabilities 中的 PlacedResources。上层调用前先确认硬件支持。

Budget

显存的容量有限。GPU驱动维护了一个内存预算(Budget),告诉应用当前可以安全使用多少显存。超出Budget不一定立即失败,但可能触发系统级的内存压力,导致其他应用被迫换出资源,甚至系统开始卡顿。

public RHIMemoryBudget QueryMemoryBudget();

public struct RHIMemoryBudget
{
    public long LocalBudget;
    public long LocalUsage;
    public long NonLocalBudget;
    public long NonLocalUsage;
}

LocalBudget/LocalUsage对应GPU本地显存,NonLocalBudget/NonLocalUsage对应系统内存中GPU可访问的部分。Budget值是动态变化的,系统中其他进程的显存占用增加时,当前进程分到的Budget会缩小。定期查询Budget并据此调整缓存策略,是一个production级渲染器必须做的事。

D3D12通过DXGI 1.4的QueryVideoMemoryInfo获取这个信息,Vulkan通过VK_EXT_memory_budget扩展,Metal没有直接对等的接口(只有recommendedMaxWorkingSetSize作为参考值)。

Residency

Budget管理的延伸是Residency控制,即显式告诉驱动哪些资源需要留在显存中,哪些可以换出。

public void RequestResidency(ReadOnlySpan<RHIResource> resources);
public void EvictResidency(ReadOnlySpan<RHIResource> resources);

RequestResidency把一批资源标记为"需要驻留",驱动会尽量保证它们在GPU可快速访问的内存中。EvictResidency做相反操作,告诉驱动这些资源短期不会被使用,可以换出到系统内存。典型场景是关卡切换:上一个关卡的资源 Evict,新关卡的资源 RequestResidency。相比销毁再重建资源,Residency操作的开销低得多,因为资源的元数据和虚拟地址空间保持不变,只有物理页面的映射发生变化。

Sparse 资源

Sparse资源(D3D12称为Reserved Resource,Vulkan称为Sparse Binding)进一步把虚拟地址空间和物理内存解耦。一个Sparse Texture可以声明为16384×16384的尺寸,但只有被Sampler Feedback标记为"正在使用"的区域才实际绑定物理内存。

public RHITexture CreateSparseTexture(
    in RHITextureDescriptor descriptor);

Sparse Texture创建后需要通过Sparse Binding操作逐块映射物理页面。页面大小由硬件决定,典型值是64KB。映射和解除映射可以在运行时动态发生,配合Sampler Feedback实现按需加载。

和Placed资源一样,Sparse也是可选能力,只有确认硬件支持后才能使用。Virtual Texture系统通常会在启动时检查Sparse Texture的Capability Tier,不支持时回退到手动的Atlas方案。

六、管线

Shader 函数

管线的输入是编译好的Shader字节码。SharpGPU通过RHIFunctionDescriptor描述一个Shader入口:

public struct RHIFunctionDescriptor
{
    public uint ByteSize;
    public IntPtr ByteCode;
    public string EntryName;
    public ERHIFunctionType Type;
    public ERHIShaderPayloadKind PayloadKind;
}

ByteCode指向编译后的Shader二进制数据(DXIL、SPIR-V或Metal IR),ByteSize是数据长度。EntryName是入口函数名,而D3D12和Vulkan的Shader模块可以包含多个入口,需要显式指定。Type区分Vertex、Fragment、Compute、RayGeneration等函数类型。PayloadKind描述Shader的Payload约定,在Ray Tracing管线中用于匹配Hit Group的数据布局。

图元装配

光栅化管线的顶点输入有两条路径:传统的顶点着色器路径和Mesh Shader路径。SharpGPU用一个判别联合RHIPrimitiveAssemblerDescriptor包装这两种选择:

public struct RHIVertexAssemblerDescriptor
{
    public RHIFunction VertexFunction;
    public Memory<RHIVertexLayoutDescriptor> VertexLayouts;
}

public struct RHIMeshletAssemblerDescriptor
{
    public RHIFunction? TaskFunction;
    public RHIFunction? MeshFunction;
}

RHIVertexAssemblerDescriptor走传统路径:VertexFunction是顶点着色器,VertexLayouts描述顶点缓冲区的布局(步长、属性偏移、输入率等)。

RHIMeshletAssemblerDescriptor走Mesh Shader路径:MeshFunction是Mesh Shader入口,TaskFunction是可选的Task(Amplification)Shader。Task Shader负责决定需要生成哪些Meshlet,Mesh Shader负责产出顶点和图元。两个字段都是可空的:MeshFunction为null时编译报错,TaskFunction为null表示不使用Task Shader阶段。

光栅化管线

RHIRasterPipeline 是不可变的编译后状态对象,对应D3D12的Pipeline State Object、Vulkan的VkPipeline、Metal的MTLRenderPipelineState。

public struct RHIRasterPipelineDescriptor
{
    public ERHISampleCount SampleCount;
    public ERHIPixelFormat DepthFormat;
    public ERHIPixelFormat[] ColorFormats;
    public RHIAttachmentInterfaceSignature AttachmentInterface;
    public RHIRasterAttachmentShaderAbiClaim? AttachmentShaderAbiClaim;
    public RHIRenderStateDescriptor RenderState;
    public RHIFunction? FragmentFunction;
    public RHIPipelineLayout? PipelineLayout;
    public RHIPrimitiveAssemblerDescriptor PrimitiveAssembler;
}

Descriptor把光栅化管线的所有状态打包在一起。SampleCount指定MSAA采样数。DepthFormat和ColorFormats声明渲染目标格式,因为管线编译需要知道输出格式才能生成正确的着色器代码和混合逻辑。AttachmentInterface是渲染目标接口签名,用于验证管线与RenderPass的兼容性。

RenderState包含完整的固定功能状态:光栅化模式(填充/线框)、剔除模式、深度测试、模板测试、混合状态等。把它打包成独立的RHIRenderStateDescriptor,是因为多个管线经常共享相同的固定功能状态。

FragmentFunction是可选的,有些Pass只写深度不写颜色(Shadow Map、Depth Pre-Pass),此时不需要Fragment Shader。PipelineLayout也是可选的,null时使用默认布局。PrimitiveAssembler是上面的判别联合,选择传统顶点路径或Mesh Shader路径。

计算管线

RHIComputePipeline 的结构比光栅化管线简单得多:

public struct RHIComputePipelineDescriptor
{
    public uint3 ThreadSize;
    public RHIFunction ComputeFunction;
    public RHIPipelineLayout PipelineLayout;
}

ThreadSize是线程组大小,三个分量对应X、Y、Z维度。ComputeFunction是Compute Shader入口。PipelineLayout描述资源绑定布局。计算管线没有固定功能状态,所有行为由Shader代码决定。

ThreadSize 显式声明在 Descriptor 中。Metal 要求在管线对象上指定线程组大小,D3D12 和 Vulkan 在 Shader 中声明。SharpGPU 统一由 Descriptor 携带,后端按各自规范处理。

管线缓存

管线编译是GPU API中最重的操作之一。一个复杂的光栅化管线可能需要数十毫秒的编译时间。RHIPipelineCache提供编译结果的持久化:

public class RHIPipelineCache
{
    public RHIPipelineCacheImportResult Import(in ReadOnlyMemory<byte> blob);
    public byte[] Export();
}

Export把当前缓存的所有管线编译结果序列化为字节数组,可以写入磁盘。Import从之前导出的数据恢复缓存,返回RHIPipelineCacheImportResult描述导入结果,包含成功导入的条目数、跳过的条目数(驱动版本不匹配或硬件不同导致的失效条目)。

应用启动时Import上次保存的缓存,关闭时Export当前缓存。这样除了首次运行,后续启动的管线编译几乎可以瞬间完成。缓存数据和GPU/驱动版本绑定,换了显卡或更新了驱动后,旧缓存会被标记为无效,不影响正确性。

七、管线布局与资源绑定

Pipeline Layout

管线布局描述Shader如何访问外部资源:哪些Descriptor Table在哪些槽位、Push Constants的大小和位置、Static Sampler的配置。

public struct RHIPipelineLayoutDescriptor
{
    public bool bLocalSignature;
    public bool bUseVertexLayout;
    public uint PushConstantSize;
    public RHIBindingTableLayout[] BindingTableLayouts;
    public Memory<RHIStaticSamplerDescriptor>? StaticSamplers;
}

bLocalSignature标记这是否为Local Root Signature。D3D12的Ray Tracing管线引入了Local Root Signature的概念,即每个Shader Record可以有独立的资源绑定,而Global Root Signature被所有Shader共享。这个标志在光栅化和计算管线中为false。

bUseVertexLayout告诉布局是否需要包含顶点输入描述。PushConstantSize指定Push Constants(D3D12的Root Constants)的字节大小,这是一小块直接嵌入命令流的常量数据,访问速度最快但容量有限(通常128到256字节)。

BindingTableLayouts是资源绑定表的布局数组。每个RHIBindingTableLayout描述一组Descriptor的排列,即SRV、UAV、CBV、Sampler各有多少个、分别在什么位置。这直接对应D3D12的Descriptor Table、Vulkan的Descriptor Set Layout、Metal的Argument Buffer Layout。

StaticSamplers是可选的静态采样器集合。传入null表示不使用静态采样器。前面§四中描述的RHIStaticSamplerDescriptor就在这里传入,成为布局的一部分。

资源绑定模型

SharpGPU的绑定模型在三个后端之间取了最大公约数。D3D12用Root Signature加Descriptor Heap,Vulkan用Pipeline Layout加Descriptor Set,Metal用Argument Buffer。核心抽象是"绑定表",即一组连续的Descriptor槽位,Shader通过寄存器索引访问。

绑定表在录制Command Buffer时设置。一次DrawCall可以引用多个绑定表(对应D3D12的多个Descriptor Table、Vulkan的多个Descriptor Set)。高频变化的资源放在靠前的表中(比如per-draw的材质参数),低频变化的放在靠后的表中(比如per-frame的全局参数),这样切换开销最小,只需要更新变化的那几个表。

Push Constants是绑定模型中的快速路径。对于每帧都变化的小量常量(MVP矩阵、时间戳、对象索引),Push Constants直接写入命令流,省掉了更新Constant Buffer、上传到GPU的步骤。代价是空间极其有限。

八、命令编码

Command Buffer

SharpGPU的命令录制模型围绕Command Buffer展开。Command Buffer是一段GPU命令的容器,录制完成后提交到队列执行。

public void Begin(string name);
public void End();

Begin开启录制,name参数用于调试标识,在PIX、RenderDoc等工具中显示这个名字,方便定位特定的Command Buffer。End结束录制,之后Command Buffer进入可提交状态。一个Command Buffer从Begin到End只能录制一次,不可重用。

Pass 结构

Command Buffer内部按Pass组织命令。每种Pass类型对应一类GPU工作:

public void BeginTransferPass(in RHITransferPassDescriptor descriptor);
public void BeginRasterPass(in RHIRasterPassDescriptor descriptor);
public void BeginComputePass(in RHIComputePassDescriptor descriptor);
public void EndPass();

每个BeginXxxPass接受对应类型的Descriptor参数。RHITransferPassDescriptor描述数据传输操作的配置。RHIRasterPassDescriptor描述渲染目标的绑定和Load/Store行为,包括哪些Attachment需要清除、哪些保留上一帧内容、渲染结束后是Store还是Discard。RHIComputePassDescriptor配置计算任务的上下文。

Pass之间不能嵌套。一个Command Buffer内可以有多个Pass,按录制顺序执行。Pass内部通过Encoder录制具体操作:Raster Pass内录制Draw Call、设置Viewport/Scissor、绑定管线和资源;Compute Pass内录制Dispatch;Transfer Pass内录制Copy、Blit操作。

命令编码器的类型由Pass类型决定。BeginRasterPass自动激活光栅化编码器,BeginComputePass自动激活计算编码器。这个设计避免了编码器类型混用导致的错误:在Raster Pass里不可能意外调到Dispatch。

编码器操作

Raster编码器提供标准的绘制操作:绑定管线、设置Viewport和Scissor矩形、绑定顶点和索引缓冲区、设置绑定表、Draw和DrawIndexed。DrawIndirect和DrawIndexedIndirect从GPU Buffer读取绘制参数,用于GPU-driven rendering。

Compute编码器相对简单:绑定计算管线、设置绑定表、Dispatch。DispatchIndirect从Buffer读取线程组数量,配合GPU-driven的工作流。

Transfer编码器处理数据搬运:Buffer到Buffer拷贝、Buffer到Texture拷贝(Upload)、Texture到Buffer拷贝(Readback)、Texture到Texture拷贝和Blit(包括格式转换和缩放)。

资源屏障在 Transfer 编码器中录制。SharpGPU 把 Barrier 拆成三种粒度,每种对应一个独立的结构体:

public enum ERHIBarrierKind : byte { Global = 0, Buffer = 1, Texture = 2 }

public struct RHIGlobalBarrier
{
    public ERHIStageMask StageBefore;
    public ERHIStageMask StageAfter;
    public ERHIAccessMask AccessBefore;
    public ERHIAccessMask AccessAfter;
}

public struct RHIBufferBarrier
{
    public RHIBuffer Resource;
    public RHIBufferRange Range;          // WholeSize = ulong.MaxValue
    public ERHIStageMask StageBefore;
    public ERHIStageMask StageAfter;
    public ERHIAccessMask AccessBefore;
    public ERHIAccessMask AccessAfter;
    public ERHIPipelineType? SourceQueue;
    public ERHIPipelineType? DestinationQueue;
}

public struct RHITextureBarrier
{
    public RHITexture Resource;
    public RHITextureSubresourceRange SubresourceRange;
    public ERHITextureLayout LayoutBefore;
    public ERHITextureLayout LayoutAfter;
    public ERHIStageMask StageBefore;
    public ERHIStageMask StageAfter;
    public ERHIAccessMask AccessBefore;
    public ERHIAccessMask AccessAfter;
    public ERHIPipelineType? SourceQueue;
    public ERHIPipelineType? DestinationQueue;
}

字段命名统一用 Before/After 对称,语义是"之前在做什么、之后要做什么"。Global Barrier 做全局的 execution + memory barrier,不指定具体资源。Buffer Barrier 精确到某个 Buffer 的某段范围。Texture Barrier 精确到 subresource range,并且多一对 layout 转换,因为 GPU 对 texture 数据有不同的压缩和排列策略,layout 切换告诉硬件重新解释底层数据。

SourceQueue/DestinationQueue 是 nullable 的 ERHIPipelineType?,非 null 时触发跨队列所有权转移。Texture 从 Compute Queue 的写入结果被 Graphics Queue 读取,需要在 Compute 侧 Release、Graphics 侧 Acquire,通过这对字段指定。

RHIBarrier 本身是一个容器,提供静态工厂方法 Global()、Buffer()、Texture() 构造对应的实例。

ERHIStageMask 有 13 个基础位:

[Flags]
public enum ERHIStageMask : ulong
{
    Transfer       = 1UL << 0,   // = Copy
    Indirect       = 1UL << 1,
    IndexInput     = 1UL << 2,
    VertexInput    = 1UL << 3,
    Vertex         = 1UL << 4,
    Fragment       = 1UL << 5,
    Compute        = 1UL << 6,
    Task           = 1UL << 7,
    Mesh           = 1UL << 8,
    RayTracing     = 1UL << 9,
    AccelStructBuild = 1UL << 10,
    AccelStructCopy  = 1UL << 11,
    MachineLearning  = 1UL << 12,
    AllGraphics = Indirect | IndexInput | VertexInput | Vertex | Fragment | Task | Mesh,
    AllShading  = Vertex | Fragment | Compute | Task | Mesh | RayTracing | MachineLearning,
    All         = ulong.MaxValue
}

ERHIAccessMask 有 18 个位:

[Flags]
public enum ERHIAccessMask : ulong
{
    IndirectCommandRead = 1UL << 0,
    IndexRead           = 1UL << 1,
    VertexRead          = 1UL << 2,
    ConstantRead        = 1UL << 3,
    ShaderRead          = 1UL << 4,
    ShaderWrite         = 1UL << 5,
    RenderTargetRead    = 1UL << 6,
    RenderTargetWrite   = 1UL << 7,
    DepthStencilRead    = 1UL << 8,
    DepthStencilWrite   = 1UL << 9,
    TransferRead        = 1UL << 10,
    TransferWrite       = 1UL << 11,
    ResolveRead         = 1UL << 12,
    ResolveWrite        = 1UL << 13,
    Present             = 1UL << 14,
    ShadingRateRead     = 1UL << 15,
    AccelStructRead     = 1UL << 16,
    AccelStructWrite    = 1UL << 17
}

ERHITextureLayout 有 13 种:

public enum ERHITextureLayout : byte
{
    Undefined, General, CopySource, CopyDestination,
    ShaderReadOnly, RenderTarget, DepthStencilReadOnly, DepthStencilWrite,
    ResolveSource, ResolveDestination, Present, ShadingRateSurface, Common
}

Vulkan 的 VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL / DST 在这里叫 CopySource / CopyDestination。Resolve 操作有独立的 ResolveSource / ResolveDestination,因为 resolve 是和 copy 不同的管线阶段。Common 对应 D3D12 enhanced barrier 引入的通用 layout。

九、队列提交与同步

队列提交

录制好的 Command Buffer 通过队列提交执行:

public readonly struct RHIQueueSemaphoreWait
{
    public RHISemaphore Semaphore { get; }
    public ERHIStageMask StageMask { get; }
}

public readonly struct RHIQueueSubmitDescriptor
{
    public ReadOnlyMemory<RHICommandBuffer> CommandBuffers { get; }
    public ReadOnlyMemory<RHIQueueSemaphoreWait> WaitSemaphores { get; }
    public ReadOnlyMemory<RHISemaphore> SignalSemaphores { get; }
    public RHIFence? CompletionFence { get; }
}

RHIQueueSemaphoreWait 把 Semaphore 和等待的管线阶段打包在一起。StageMask 指定在哪个管线阶段等待,GPU 在不需要数据的前序阶段可以提前执行,只在真正消费数据的地方等。

所有字段都为空时(无 Command Buffer、无 Wait/Signal、无 Fence),提交抛异常:"A queue submission must contain work or a synchronization operation." 空提交是上层逻辑 Bug。

提交内部是三阶段事务。Validate 阶段校验所有 CommandBuffer 为 Executable 状态、所有 Semaphore/Fence 未被其他提交占用。Reserve 阶段对所有 Semaphore 执行 ReserveSignal/ReserveWait、对 Fence 执行 ReserveSignal,任一失败则全部 Rollback。最后调用后端原生提交,成功后 Commit 所有预约,失败则 Rollback。这套事务语义确保不会出现"Semaphore 已消费但 CommandBuffer 实际未提交"的悬空状态。

BindSparse 用于队列序 sparse 绑定,是可选能力,对应 Capabilities 里的 SparseAliasing。

Fence

Fence是CPU-GPU之间的同步原语。SharpGPU使用二元状态的Fence,生命周期为:Ready → Pending → Signaled → Resetting → Ready。

public class RHIFence
{
    public ERHIFenceStatus Status { get; }
    public void Reset();
    public bool Wait(ulong timeoutNanoseconds);
}

新创建的Fence处于Ready状态。作为CompletionFence提交到队列后进入Pending,GPU完成对应的Command Buffer后变为Signaled。CPU调用Wait等待Signaled状态,timeoutNanoseconds指定超时时间,返回true表示在超时前收到信号,false表示超时。Wait返回后调用Reset把Fence重置回Ready,准备下一次使用。

这个生命周期是强制的:对Pending状态的Fence调用Reset会抛异常,对Ready状态的Fence调用Wait会立即返回。状态机约束消除了一类难以调试的同步错误。

Semaphore

Semaphore是GPU-GPU之间的同步原语,用于跨队列依赖。SharpGPU的Semaphore采用二元模型,通过Reserve/Commit/Rollback三阶段协议管理信号操作:

public class RHISemaphore
{
    // Signal 侧
    public void ReserveSignal();
    public void CommitSignal();
    public void RollbackSignal();

    // Wait 侧
    public void ReserveWait();
    public void CommitWait();
    public void RollbackWait();
}

Reserve预订一个信号操作但不立即执行,此时Semaphore被锁定,其他Queue不能重复预订。Commit确认操作,在Queue实际执行提交时生效。Rollback取消预订,释放锁定。

三阶段协议的设计动机是处理提交失败的情况。录制Command Buffer时预订Semaphore的Signal和Wait,如果录制过程中发生错误需要丢弃整个提交,Rollback可以干净地撤销预订,不会留下悬空的同步状态。如果没有这层保护,一个预订了Signal但没有实际提交的Semaphore会导致Wait端永久阻塞。

典型的跨队列同步流程:Compute Queue的提交ReserveSignal一个Semaphore,Graphics Queue的提交ReserveWait同一个Semaphore。两边录制完成后分别CommitSignal和CommitWait,然后提交。GPU执行时,Graphics Queue会在到达Wait点时暂停,直到Compute Queue完成Signal。

十、查询与时间戳

时钟校准

GPU有自己的时钟域,和CPU时钟不同步。要把GPU时间戳转换成CPU时间需要校准:

public struct RHIClockCalibration
{
    public ERHIPipelineType Queue;
    public int QueueIndex;
    public ulong GpuTimestamp;
    public ulong CpuTimestamp;
    public ulong GpuFrequency;
}

Queue字段是ERHIPipelineType类型,标识校准针对的队列种类(Graphics、Compute、Transfer)。QueueIndex是同类型队列中的索引。GpuTimestamp和CpuTimestamp是同一时刻在两个时钟域的采样值,GpuFrequency是GPU时钟的频率(每秒tick数)。通过这组数据可以把任意GPU时间戳转换为CPU时间。

校准数据有时效性,两个时钟的漂移会随时间累积。在性能分析场景中,每帧或每几帧重新校准一次比较稳妥。

Query 对象

RHIQuery 用于从GPU收集性能数据:

public class RHIQuery
{
    public ReadOnlyMemory<ulong> Results { get; }
    public bool TryGetTimestamp(uint index, out ulong timestamp);
    public bool TryGetOcclusion(uint index, out ulong sampleCount);
    public bool TryGetPipelineStatistics(uint index, out RHIPipelineStatistics stats);
}

Query在Command Buffer中通过BeginQuery/EndQuery括住一段命令。GPU执行完成后,结果填入Query对象的内部缓冲区。Results属性以ReadOnlyMemory<ulong>的形式暴露原始数据。

TryGet系列方法提供类型安全的访问。index参数(uint类型)指定查询槽位,一个Query对象可以包含多个槽位,比如一帧内多个Draw Call各对应一个Occlusion Query槽位。返回false表示数据尚未就绪(GPU还没执行到那里),上层应该下一帧再来读取。

时间戳查询返回GPU时钟的tick值,配合RHIClockCalibration转换为CPU时间。遮挡查询返回通过深度测试的采样数,为零表示完全被遮挡,可以跳过后续的绘制。管线统计查询返回RHIPipelineStatistics结构,包含顶点处理数、光栅化像素数、Shader调用数等性能指标。

十一、加速结构

光线追踪的两级结构

硬件光线追踪依赖加速结构(Acceleration Structure)来高效求交。SharpGPU采用业界标准的两级结构:Bottom Level Acceleration Structure(BLAS)持有几何数据,Top Level Acceleration Structure(TLAS)持有实例引用和变换矩阵。

BLAS在创建时接收三角形网格或AABB数据,GPU驱动内部构建BVH。一个BLAS对应一个几何体的不同LOD或变体,比如一棵树的树干和叶片可以放在同一个BLAS的不同Geometry中。

TLAS引用一组BLAS实例,每个实例附带变换矩阵和材质索引。场景中1000棵相同的树可以共享一个BLAS,在TLAS中用1000个不同变换的Instance引用它。

实例变换

TLAS中每个实例的变换通过float4x4矩阵描述:

public struct RHIAccelerationStructureInstance
{
    public float4x4 TransformMatrix;
    public uint InstanceId;
    public uint InstanceMask;
    public uint HitGroupIndex;
    public ERHIAccelerationStructureInstanceFlags Flags;
    public RHIAccelerationStructure BottomLevel;
}

TransformMatrix是完整的4×4变换矩阵。用float4x4而非3×4矩阵(如DXR标准中的FLOAT3X4),是因为SharpGPU在抽象层面保持数学类型的一致性,内部数学库统一使用float4x4,向DXR提交时截取前三行即可。Vulkan的VkTransformMatrixKHR同样是3×4,Metal的MTLAccelerationStructureInstanceDescriptor也用4×3列主序。后端实现负责格式转换。

InstanceId是用户定义的标识符,Shader中通过InstanceID()内建函数读取。InstanceMask是8位掩码,TraceRay时可以按掩码过滤实例,用于实现分层可见性(比如反射中不显示某些对象)。HitGroupIndex指定该实例使用的Hit Group,对应Shader Binding Table中的入口。

构建与更新

加速结构的构建是异步操作,在Compute或Transfer命令中执行:

public void BuildAccelerationStructure(
    in RHIAccelerationStructureBuildDescriptor descriptor);

public void UpdateAccelerationStructure(
    in RHIAccelerationStructureBuildDescriptor descriptor);

Build从头构建BVH,耗时较长但生成最优结构。Update就地更新已有BVH,只要几何数据的拓扑不变(顶点可以移动但三角形连接关系不变)时可以使用,速度比全量Build快很多,适合骨骼动画等场景。频繁Update后BVH质量会下降,定期做全量Rebuild是必要的。

十二、交换链与呈现

交换链

交换链(SwapChain)管理呈现到屏幕上的帧缓冲区。它是GPU抽象层中与操作系统窗口系统耦合最紧的部分。

public struct RHISwapChainDescriptor
{
    public bool FrameBufferOnly;
    public uint FPS;
    public uint Count;
    public uint2 Extent;
    public ERHINativeSurfaceKind SurfaceKind;
    public IntPtr WindowHandle;
    public IntPtr DisplayHandle;
    public IntPtr InstanceHandle;
    public uint SurfaceGeneration;
    public ERHIPresentMode PresentMode;
    public ERHISwapChainFormat Format;
    public RHICommandQueue PresentQueue;
}

Count是帧缓冲区数量,典型值是2(双缓冲)或3(三缓冲)。Extent是渲染分辨率。FrameBufferOnly为true时,帧缓冲区只能作为RenderTarget,不能被Shader读取。这在某些平台上能解锁更高效的呈现路径。

WindowHandle、DisplayHandle、InstanceHandle是平台原生的窗口句柄。Windows上WindowHandle是HWND,Linux上还需要DisplayHandle(X11的Display或Wayland的wl_display)和InstanceHandle。SurfaceKind标识窗口系统类型(Win32、X11、Wayland、Cocoa等)。

SurfaceGeneration是一个递增计数器,窗口重建时递增。它用来检测SwapChain是否需要重新创建,比如窗口大小改变、DPI切换、甚至窗口在不同显示器之间拖动都可能需要重建SwapChain。

PresentMode指定呈现模式:Immediate不等待VSync直接翻转(最低延迟但会撕裂),Fifo等待VSync后翻转(D3D12的SyncInterval=1),Mailbox在等待VSync的同时保持最新帧(SyncInterval=0但不撕裂)。FPS字段在某些模式下限制帧率上限。

PresentQueue指定用于呈现的命令队列,渲染的最后一步是在这个队列上提交Present操作。多GPU系统中这决定了哪个GPU负责最终的屏幕输出。

交换链状态

Present操作返回交换链的状态:

public enum ERHISwapChainStatus
{
    Undefined,
    Success,
    NotReady,
    Timeout,
    Occluded,
    Suboptimal,
    OutOfDate,
    SurfaceLost,
    DeviceLost
}

Success是正常路径。NotReady表示帧缓冲区暂时不可用(上一帧还没显示完)。Timeout表示等待帧缓冲区超时。Occluded表示窗口被完全遮挡,可以降低渲染频率节省功耗。

Suboptimal表示SwapChain仍然可用但不是最优配置,通常发生在窗口大小改变后、SwapChain还没重建的过渡期。此时渲染结果会被缩放到窗口大小,有性能损失。收到Suboptimal应该尽快重建SwapChain。

OutOfDate表示SwapChain已经过期,必须重建才能继续Present。SurfaceLost表示底层窗口Surface已失效。DeviceLost表示GPU设备丢失,需要从Device层开始完整重建。Vulkan后端中,OutOfDate和SurfaceLost分别对应VK_ERROR_OUT_OF_DATE_KHR和VK_ERROR_SURFACE_LOST_KHR。

窗口频繁调整大小时,从Suboptimal到重建完成的路径开销需要尽可能小:重建SwapChain时只销毁帧缓冲区相关资源,不影响管线、绑定布局等长期存活的对象。

十三、高级特性

Attachment Shader ABI 契约

SharpGPU引入了RHIRasterAttachmentShaderAbiClaim来确保渲染目标配置和Shader之间的ABI兼容性:

public struct RHIRasterAttachmentShaderAbiClaim
{
    public string ContractHash;
    public ERHIPixelFormat[] ColorFormats;
    public ERHIPixelFormat DepthFormat;
    public ERHISampleCount SampleCount;
}

ContractHash是一个64字符的十六进制字符串,编码了Attachment接口的完整签名。编译管线时,Shader的输出声明和RenderPass的Attachment配置各自生成一个ContractHash。两边的Hash必须匹配,如果Shader声明输出4个Color Target但RenderPass只绑定了3个,Hash不同,创建管线时报错。

用字符串Hash而非逐字段比较,是因为Attachment接口涉及的参数组合很多(颜色格式列表、深度格式、采样数、各种兼容性标志),逐字段比较容易遗漏。64字符的十六进制字符串(256位)碰撞概率忽略不计。

Cooperative Matrix

Cooperative Matrix是Vulkan VK_KHR_cooperative_matrix和D3D12 Cooperative Vectors扩展暴露的功能,让Shader的一个Subgroup协作完成矩阵运算。对ML推理和矩阵密集型计算很有用。

public struct RHICooperativeMatrixConfig
{
    public uint M;
    public uint N;
    public uint K;
    public ERHICooperativeMatrixElementType AType;
    public ERHICooperativeMatrixElementType BType;
    public ERHICooperativeMatrixElementType CType;
    public ERHICooperativeMatrixElementType ResultType;
    public ERHICooperativeMatrixScope Scope;
}

M、N、K定义矩阵乘法的维度(C[M×N] = A[M×K] × B[K×N])。AType/BType/CType/ResultType分别描述四个操作数的元素类型,类型为ERHICooperativeMatrixElementType,涵盖float16、float32、int8、int32等精度选项。不同的GPU架构支持不同的类型组合,例如Nvidia的Tensor Core对fp16×fp16→fp32最高效,AMD的WMMA指令集则有不同的偏好。

Scope是ERHICooperativeMatrixScope类型,描述参与协作的线程范围,可以是Subgroup级别或更小的范围。硬件的Subgroup大小和矩阵维度之间有严格的对应关系,上层必须通过Capabilities查询当前硬件支持哪些MNK组合和类型搭配。

Work Graph

Work Graph是D3D12引入的GPU-driven工作调度机制。传统模型下CPU决定Dispatch参数,Work Graph允许GPU自行产出新的工作负载,一个计算节点的输出直接驱动下一个节点的输入,不需要CPU介入。

SharpGPU通过RHIWorkGraphCapabilities暴露Work Graph支持的Tier,通过Device.CreateWorkGraphPipeline创建Work Graph管线。管线中的每个节点对应一个Compute Shader,节点之间的数据流通过Record类型声明。

Work Graph的典型应用包括GPU-driven culling(遮挡剔除后直接产出DrawIndirect参数)、自适应曲面细分(根据屏幕空间大小动态决定细分级别)、粒子系统(粒子生成和销毁完全在GPU上决策)。

Function Library

Function Library在§四的View系统中已经提过。补充它在管线中的角色:D3D12的Work Graphs和DXR 1.1引入了"函数即一等公民"的概念,即Shader函数可以编译到一个库中,运行时按名称和类型选取。Metal的Function Tables也是类似的机制。

public class RHIFunctionLibrary
{
    public RHIFunction CreateFunction(in RHIFunctionViewDescriptor descriptor);
}

FunctionLibrary 从一组 Shader 源码编译得到,CreateFunction 通过 RHIFunctionViewDescriptor(指定 EntryName 和 Type)从中提取特定入口的 RHIFunction 对象。这个模型的好处是共享编译成本:一个Library包含了所有可能用到的函数,编译一次后按需取用。对于Work Graph和Ray Tracing管线中大量的Shader变体,这比每个变体单独编译高效得多。

FunctionLibrary的Capability Tier在RHIFunctionLibraryCapabilities中描述。Tier1支持基本的函数选取,高Tier支持动态链接、函数指针等高级特性。

Indirect Command Buffer

Indirect Command Buffer(ICB)用于多线程录制场景,将大量绘制命令分段为 ParallelTask 并行录制,缓解 DrawCall 录制压力。使用方式对齐三大 API,在 Encoder 中通过 Execute 调用。

多线程录制的核心策略是将一帧的 DrawCall 按区间分段,每个线程获得一个独立的 ICB 片段进行录制,录制完毕后主线程在 Encoder 中按顺序 Execute 所有片段。分段粒度通常按 DrawCall 数量均分(如 8000 DrawCall / 4 线程 = 每段 2000),也可按场景空间划分以提升 Cache 命中率。Execute 的语义是将 ICB 中已录制的命令嵌入当前 Encoder 的命令流,GPU 侧顺序执行。ICB 内的命令不能跨 RenderPass 边界,分段时必须保证每个片段的命令归属同一个 Pass。Metal 的 ICB 还要求预先指定最大命令数量,超出会静默丢弃。


十四、启动渲染流程

完整的初始化到渲染的流程:

  1. 创建 RHIInstance,后端类型可显式指定或通过 GetBackendByPlatform 自动选取。
  2. 通过 Instance 枚举并选择 GPU,基于 GPU 创建 Device。
  3. 从 Device 获取 Queue 数量,通过 Index 取到具体 Queue 实例。
  4. 根据平台创建 Surface,关联到 SwapChain,指定缓冲数量(通常 triple buffering)。
  5. 为每个缓冲创建 RenderTarget 类型的 TextureView,访问 SwapChain 资源。
  6. 离线编译好的 Shader IL 通过 Descriptor 创建 Shader 对象。Shader 接收纯二进制数据,开发者可自由设计自己的 shader 编译框架。
  7. 从目标 Queue 获取 CommandPool,取得 CommandBuffer,建立 Encoder 录制所需的各 Pass。

我实际跑了一个 Compute + Graphics 的案例,RenderDoc 里能看到一个 FrameRendering 的 DebugLabel,内部两个 Pass:

  • ComputePass:输出 UV 数据到 Texture。
  • GraphicsPass:渲染三角形,采样 ComputePass 输出的 UV Texture,结合顶点颜色输出最终结果。

从 Instance 创建到 RenderDoc 里看到完整的 Pass 结构,中间经过的对象和调用顺序,基本就是前面十三章描述的那些东西串起来的样子。


写在最后

开头提到 GPU 平台 API 抽象的复杂度有四个来源:状态组合爆炸、异构调度、架构差异、功能不对称。SharpGPU 的 Abstract 层对这四个方向分别做了处理。

状态组合爆炸的应对是把所有可变状态收进不可变的 Pipeline 对象。RHIRasterPipelineDescriptor 把混合、光栅化、深度模板、输出格式、采样数一次性锁定,运行时不存在零散的 SetRenderState 调用。Mesh Shader 和传统顶点路径共用同一个 RasterPipeline 类型,通过 PrimitiveAssembler 区分,而不是再多一种管线。

异构调度的应对是把 Graphics、Compute、Transfer 三种队列和六种 Encoder 做成正交的组合。Command Buffer 内按 Pass 切分,同一时刻只有一个活跃 Encoder,类型由 Pass 决定。提交层的 Semaphore 用二元 Reserve/Commit/Rollback 协议管理跨队列依赖,Fence 管理 CPU-GPU 同步。Queue 之间没有隐式关系,所有依赖都在提交描述符里显式声明。

架构差异的应对是把 IMR 和 TBDR 的分歧压到后端去处理,Abstract 层提供的是两边都能接受的语义。ERHITextureLayout 只有 13 种,不暴露 Vulkan 独有的 DepthReadOnlyStencilReadWrite 之类的组合状态。Barrier 结构用 Before/After 对称命名,跨队列所有权转移用 nullable 的 SourceQueue/DestinationQueue 表达。Metal 后端对冗余 Barrier 做静默优化,上层不需要关心当前跑在 IMR 还是 TBDR 上。

功能不对称的应对是 14 个域的 Capability 模型。每个域独立的 Tier 等级加上 Provenance 来源追踪,上层按 Tier 选路径。Placed、Sparse、Residency、Budget 这些可选能力走同一套查询机制,不支持的后端直接报 Unavailable 和具体原因。资源级的 QueryFormatSupport / QueryResolveSupport / QueryRasterAttachmentSupport 把"这个具体参数组合能不能用"的问题从 Capabilities 的设备级粒度进一步细化到单次创建级。