- 示例工程
【免费下载链接】DirectX-Graphics-Samples
This repo contains the DirectX Graphics samples that demonstrate how to build graphics intensive applications on Windows.
本篇技术指南以 DirectX-Graphics-Samples 仓库中的 D3D12nBodyGravity 示例 为对象,深入讲解如何利用 Direct3D 12 的异步计算着色器(多引擎,multi-engine)在专用计算命令队列上并行模拟 10,000 个粒子的 n-body 引力系统,同时用跨命令队列的 Fence 同步机制协调计算与渲染两条流水线,并兼顾 Root Signature 1.1 与 Enhanced Barriers 两项进阶特性。读完本文,你将掌握多引擎异步计算应用的完整架构、双缓冲乒乓与 SRV/UAV 切换模式、Fence 跨队列同步写法,以及如何在硬件不支持时优雅降级到传统资源屏障。
示例概览:为什么要用多引擎跑 n-body 模拟
n-body 引力模拟(每个粒子受所有其他粒子引力作用)是典型的计算密集型负载,非常适合交给 GPU 的异步计算队列(D3D12_COMMAND_LIST_TYPE_COMPUTE)处理。示例 readme 明确指出其核心价值在于两点(readme.md):
- 图形命令与计算命令可同时录制:渲染线程在图形命令队列(DIRECT)上录制绘制命令,独立的工作线程在计算命令队列上录制并提交模拟命令,GPU 在两个队列间并行执行;
- 跨命令队列的 Fence 同步:用 Fence 保证"计算写出的数据在渲染读取之前已完成"以及"渲染读取结束后计算才能覆写",避免数据竞争。
从源码结构看,示例的核心骨架集中在 D3D12nBodyGravity.h 与 D3D12nBodyGravity.cpp 中:主类继承DXSample,通过OnInit/OnUpdate/OnRender/OnDestroy生命周期方法组织整个应用。
整体架构:渲染主线程 + 异步计算工作线程
示例采用"一主一从"的线程模型:主线程负责窗口、输入、相机、渲染与 Present;后台计算线程持续运行模拟循环。头文件中的关键常量(D3D12nBodyGravity.h)奠定了数据规模:
static const UINT FrameCount = 2; // 交换链双缓冲 static const UINT ThreadCount = 1; // 异步计算线程(上下文)数量 static const float ParticleSpread; // 初始粒子散布半径(实现中取 400.0f) static const UINT ParticleCount = 10000; // n-body 模拟的粒子总数ThreadCount是可扩展的:每个线程拥有独立的计算命令队列、命令分配器、命令列表与 Fence(见 D3D12nBodyGravity.h),渲染端会按ThreadCount把屏幕切分为若干视口,每个视口绘制一个计算上下文产生的粒子数据(D3D12nBodyGravity.cpp)。本示例ThreadCount = 1时即单视口全屏渲染。
初始粒子布局由 CreateParticleBuffers 完成:将 10,000 个粒子平均分成两团,分别以(±ParticleSpread×0.5, 0, 0)为中心、以±20的 z 轴初速度相向运动,从而在引力作用下形成交互聚拢的双星系统——这是观察引力模拟最直观的初始条件。
双缓冲乒乓:SRV/UAV 交替读写
模拟的正确性依赖"读写分离":计算着色器把上一帧的粒子数据(SRV)读入,把新位置/速度写入另一块缓冲(UAV),渲染端则绘制没有被计算线程正在写入的那块缓冲。因此每个计算上下文维护两块粒子缓冲m_particleBuffer0/1,并用m_srvIndex记录当前 SRV 是 0 号还是 1 号缓冲(D3D12nBodyGravity.h)。
描述符堆中的资源视图按以下索引排布(D3D12nBodyGravity.h):
enum DescriptorHeapIndex : UINT32 { UavParticlePosVelo0 = 0, UavParticlePosVelo1 = UavParticlePosVelo0 + ThreadCount, SrvParticlePosVelo0 = UavParticlePosVelo1 + ThreadCount, SrvParticlePosVelo1 = SrvParticlePosVelo0 + ThreadCount, DescriptorCount = SrvParticlePosVelo1 + ThreadCount };每次模拟结束后计算线程执行m_srvIndex[threadIndex] = 1 - m_srvIndex[threadIndex](D3D12nBodyGravity.cpp),实现两块缓冲的角色互换。粒子数据格式为Particle{ float4 position; float4 velocity; }(D3D12nBodyGravity.h),其中position.w存放质量相关参数、velocity.w存放加速度模长(供渲染端着色粒子颜色)。
计算着色器内核:tile 化 n-body 引力求解
核心计算内核位于 nBodyGravityCS.hlsl,它实现了经典的"块缓存"(tile-based)O(N²) 引力求解,以共享内存减少全局内存读取:
#define blocksize 128 groupshared float4 sharedPos[blocksize];每个线程负责一个粒子的位置与速度更新。外层循环按 tile 遍历所有粒子,每次把一个 128 粒子的 tile 拷入groupshared共享内存,再内层循环完成该 tile 内所有粒子的两两引力计算(nBodyGravityCS.hlsl)。物理参数在文件顶部定义:
static float softeningSquared = 0.0012500000f * 0.0012500000f; // 软化项,避免奇点 static float g_fG = 6.67300e-11f * 10000.0f; // 万有引力常数(放大) static float g_fParticleMass = g_fG * 10000.0f * 10000.0f; // 粒子质量核心的质点间引力更新函数bodyBodyInteraction采用经典的r / |r|³形式并加入软化项(softening)防止距离趋零时加速度发散(nBodyGravityCS.hlsl):
void bodyBodyInteraction(inout float3 ai, float4 bj, float4 bi, float mass, int particles) { float3 r = bj.xyz - bi.xyz; float distSqr = dot(r, r); distSqr += softeningSquared; float invDist = 1.0f / sqrt(distSqr); float invDistCube = invDist * invDist * invDist; float s = mass * invDistCube * particles; ai += r * s; }计算着色器通过常量缓冲cbCS接收每帧参数(nBodyGravityCS.hlsl),对应 CPU 端 ConstantBufferCS 的填充:
| 成员 | 值 | 含义 |
|---|---|---|
param[0] | ParticleCount(10000) | 粒子总数(MAX_PARTICLES) |
param[1] | ceil(ParticleCount / 128.0f)(79) | tile 数量,即外层循环次数 |
paramf[0] | 0.1f | 时间步长 deltaTime |
paramf[1] | 1.0f | 速度阻尼系数 damping |
内核末尾有一个容易踩坑的细节:当ParticleCount不是blocksize整数倍时,tile 循环会产生越界读——而计算着色器中越界读返回 0,相当于在原点 (0,0,0) 凭空多出若干"幽灵粒子"引力源,因此需要显式回减(nBodyGravityCS.hlsl):
const int tooManyParticles = g_param.y * blocksize - g_param.x; bodyBodyInteraction(accel, float4(0, 0, 0, 0), pos, mass, -tooManyParticles);随后按vel += accel * dt; vel *= damping; pos += vel * dt完成欧拉积分(nBodyGravityCS.hlsl),并把加速度模长写入velo.w供渲染使用。Dispatch 线程组数为ceil(ParticleCount / 128),即 79 组(D3D12nBodyGravity.cpp)。
渲染管线:几何着色器生成点精灵
粒子绘制走"顶点着色器 + 几何着色器 + 像素着色器"三段管线,着色器见 ParticleDraw.hlsl:
- 顶点着色器:以
SV_VERTEXID索引结构化缓冲g_bufPosVelo(即计算线程刚写好的粒子缓冲),取出位置与加速度模长,按mag = velo.w / 9在红色与基础色之间插值,实现"高速粒子更红"的视觉反馈(ParticleDraw.hlsl); - 几何着色器:把每个点展开为 4 个顶点的公告板四边形(billboard),用逆视图矩阵让粒子始终面向相机,再经
worldViewProjection变换输出(ParticleDraw.hlsl); - 像素着色器:依据纹理坐标生成径向渐变,形成柔和光斑效果(ParticleDraw.hlsl)。
对应图形 PSO 配置:混合模式为源 alpha 加性混合(SrcBlend = SRC_ALPHA, DestBlend = ONE),关闭深度写入(D3D12nBodyGravity.cpp),每帧以DrawInstanced(ParticleCount, 1, 0, 0)一次性绘制全部 10,000 个粒子(D3D12nBodyGravity.cpp)。
跨命令队列的 Fence 同步:计算与渲染的握手协议
这是示例最值得研究的部分,完整实现分布在 AsyncComputeThreadProc 与 OnRender 中。整个同步协议包含两个方向的等待:
- 渲染等待计算:
OnRender开头检查每个计算线程的 Fence 完成值,若当前帧模拟未完成,则调用m_commandQueue->Wait(m_threadFences[n], threadFenceValue)让渲染队列等待计算队列的信号(D3D12nBodyGravity.cpp),保证 SRV 数据已就绪; - 计算等待渲染:计算线程提交模拟后
Signal自己的 Fence 并阻塞等待完成;随后检查渲染队列的 Fence,若渲染线程尚未释放该缓冲的 SRV 读取权,则调用pCommandQueue->Wait(m_renderContextFence, renderContextFenceValue)让计算队列等待渲染队列的信号(D3D12nBodyGravity.cpp),避免下一轮模拟覆写正在被渲染的缓冲。
计算线程主循环伪代码(D3D12nBodyGravity.cpp):
loop until m_terminating: Simulate(threadIndex) // 录制:屏障 + Dispatch 粒子模拟 提交计算命令列表 threadFenceValue = InterlockedIncrement(&m_threadFenceValues[i]) computeQueue->Signal(threadFence, threadFenceValue) // 通知渲染:模拟完成 SetEventOnCompletion(threadFenceValue); WaitForSingleObject(...) if (renderContextFence 未完成该帧): // 渲染还在读 SRV computeQueue->Wait(renderContextFence, renderContextFenceValue) m_srvIndex[i] = 1 - m_srvIndex[i] // 交换 SRV/UAV 角色 重置命令分配器与命令列表注意这里使用了InterlockedIncrement/InterlockedExchange/InterlockedCompareExchange等原子操作(头文件顶部定义了InterlockedGetValue宏,D3D12nBodyGravity.cpp),确保跨线程读取 Fence 值安全。Fence 对象本身以D3D12_FENCE_FLAG_SHARED创建(D3D12nBodyGravity.cpp),保证跨队列可等待。
资源生命周期管理同样值得借鉴:头文件注释明确提醒ComPtr只管理 CPU 侧生命周期,GPU 仍可能引用待销毁对象,因此OnDestroy先置m_terminating终止计算线程、再WaitForRenderContext确保 GPU 排空后才释放资源(D3D12nBodyGravity.h、D3D12nBodyGravity.cpp)。
可选特性一:Root Signature 1.1 与静态数据声明
readme 提到示例已面向 Windows 10 周年更新 SDK 构建,并启用了 Root Signature 1.1。Root Signature 1.1 允许应用向驱动声明"描述符不会变"或"描述符指向的数据不会变",驱动据此做出原本不可能的优化(例如提前缓存地址、减少重绑定开销)。
示例在 LoadAssets 中先通过CheckFeatureSupport(D3D12_FEATURE_ROOT_SIGNATURE, ...)探测最高可用版本,最高请求D3D_ROOT_SIGNATURE_VERSION_1_1,失败则回退到D3D_ROOT_SIGNATURE_VERSION_1_0,再调用D3DX12SerializeVersionedRootSignature序列化——这是一套标准的版本协商降级模式。
两套根签名分别服务于图形与计算管线,且使用了不同的 Volatile/Static 声明策略:
| 根参数 | 类型 | 标志 | 说明 |
|---|---|---|---|
图形GraphicsRootCBV | 常量缓冲视图 | D3D12_ROOT_DESCRIPTOR_FLAG_DATA_STATIC | 每帧更新但数据地址稳定,可静态 |
图形GraphicsRootSRVTable | SRV 描述符表 | D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC | 粒子数据按帧切换,指向数据视为静态 |
计算ComputeRootCBV | 常量缓冲视图 | D3D12_ROOT_DESCRIPTOR_FLAG_DATA_STATIC | 模拟参数初始化后不再变化 |
计算ComputeRootSRVTable | SRV 描述符表 | D3D12_DESCRIPTOR_RANGE_FLAG_DESCRIPTORS_VOLATILE | 描述符逐帧切换,声明为 Volatile |
计算ComputeRootUAVTable | UAV 描述符表 | D3D12_DESCRIPTOR_RANGE_FLAG_DATA_VOLATILE | 每帧由计算写入,指向数据声明为 Volatile |
关键代码(D3D12nBodyGravity.cpp):
ranges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0, 0, D3D12_DESCRIPTOR_RANGE_FLAG_DESCRIPTORS_VOLATILE); ranges[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0, 0, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_VOLATILE);Root Signature 1.1 的价值正体现在这里:描述符语义被显式化,驱动可以针对DATA_STATIC的绑定做激进优化,而VOLATILE声明则明确告诉驱动"每次都必须重新解析",语义清晰且安全。
可选特性二:Enhanced Barriers 与 Legacy Barriers 双路径
readme 指出:硬件支持时示例使用 Enhanced Barriers,否则回退到 Legacy Barriers。示例在LoadPipeline中通过D3D12_FEATURE_D3D12_OPTIONS12探测EnhancedBarriersSupported(D3D12nBodyGravity.cpp),并以m_bIsEnhancedBarriersEnabled贯穿全部资源创建与屏障代码。
资源创建路径差异:Enhanced Barriers 路径使用新 APICreateCommittedResource3配合D3D12_BARRIER_LAYOUT_UNDEFINED初始布局;Legacy 路径则用CreateCommittedResource并直接指定初始状态(如D3D12_RESOURCE_STATE_COPY_DEST)(D3D12nBodyGravity.cpp)。
屏障指令差异:Enhanced Barriers 路径调用m_commandList->Barrier(...)提交D3D12_BUFFER_BARRIER/D3D12_TEXTURE_BARRIER,显式给出SyncBefore/SyncAfter/AccessBefore/AccessAfter;Legacy 路径则调用ResourceBarrier做状态转换(Transition)。例如粒子缓冲由 Copy 转入可读状态时(D3D12nBodyGravity.cpp):
// Enhanced Barriers CD3DX12_BUFFER_BARRIER( D3D12_BARRIER_SYNC_COPY, // SyncBefore D3D12_BARRIER_SYNC_NON_PIXEL_SHADING, // SyncAfter D3D12_BARRIER_ACCESS_COPY_DEST, // AccessBefore D3D12_BARRIER_ACCESS_SHADER_RESOURCE, // AccessAfter m_particleBuffer0[index].Get()); // Legacy Barriers m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition( m_particleBuffer0[index].Get(), D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE));一个值得注意的优化细节:帧首屏障使用D3D12_BARRIER_SYNC_NONE与D3D12_BARRIER_ACCESS_NO_ACCESS(D3D12nBodyGravity.cpp),源码注释明确说明:这样能避免 Legacy Barrier 必须 flush 已完成工作的隐式开销——Enhanced Barriers 把同步语义拆分成 Sync 与 Access 两个维度,允许在确实无依赖时跳过不必要的同步等待,这正是新屏障模型相对旧模型的精度优势。计算阶段的 UAV 读写屏障同样成对出现:Dispatch 前SHADER_RESOURCE → UNORDERED_ACCESS,Dispatch 后反向(D3D12nBodyGravity.cpp),确保读写顺序正确。
构建与运行
示例源码位于 Samples/Desktop/D3D12nBodyGravity/src,工程文件为 D3D12nBodyGravity.vcxproj,支持 Debug/Release × x64/ARM64 四套配置。依赖以下 NuGet 包(由工程文件中的EnsureNuGetPackageBuildImports目标强制校验):
Microsoft.Direct3D.D3D12.1.618.3(D3D12 Agility SDK)Microsoft.Direct3D.DXC.1.8.2505.32(HLSL 编译器)WinPixEventRuntime(PIX 事件标注支持)
着色器在构建期由dxc.exe编译为.cso(工程内 CustomBuild 步骤,D3D12nBodyGravity.vcxproj):
# 计算着色器 dxc.exe -nologo -Tcs_6_0 -E"CSMain" -Zi -Qembed_debug -Fo"nBodyGravityCS.cso" nBodyGravityCS.hlsl # 绘制着色器(VS/GS/PS 三段) dxc.exe -nologo -Tvs_6_0 -E"VSParticleDraw" -Zi -Qembed_debug -Fo"ParticleDraw_VS.cso" ParticleDraw.hlsl dxc.exe -nologo -Tgs_6_0 -E"GSParticleDraw" -Zi -Qembed_debug -Fo"ParticleDraw_GS.cso" ParticleDraw.hlsl dxc.exe -nologo -Tps_6_0 -E"PSParticleDraw" -Zi -Qembed_debug -Fo"ParticleDraw_PS.cso" ParticleDraw.hlsl运行时说明:
- 直接用 Visual Studio 打开工程生成并运行;代码通过
D3D12SDKVersion = 618与D3D12SDKPath = ".\\D3D12\\"加载随包提供的 Agility SDK(D3D12nBodyGravity.cpp); - 命令行传入
-warp(或/warp)可强制使用 WARP 软件光栅化适配器,便于在无独显的机器上验证逻辑(DXSample.cpp); - 示例调用
DXGIDeclareAdapterRemovalSupport声明支持设备移除,并在OnRender捕获DXGI_ERROR_DEVICE_REMOVED/RESET后通过RestoreD3DResources重建全部 D3D 资源(D3D12nBodyGravity.cpp); - Debug 构建会启用 D3D12 调试层(需要系统安装 Graphics Tools 可选功能),详见 D3D12nBodyGravity.cpp。
键盘操作为 WASD 移动相机、鼠标观察,示例内嵌的SimpleCamera与StepTimer负责视角与帧计时(D3D12nBodyGravity.cpp)。
小结:这套架构能复用到哪里
D3D12nBodyGravity 示例的价值不止于粒子效果本身,其背后是可复用的通用模式:
- 多引擎异步计算:把与渲染解耦的 GPU 计算(物理、粒子、流体、后处理预处理等)放到独立 COMPUTE 队列,与图形队列并行执行,充分压榨 GPU 空闲带宽;
- 跨队列 Fence 握手:渲染端
Queue::Wait(computeFence)+ 计算端Queue::Wait(renderFence)的双向同步协议,是任何多队列应用都必须掌握的范式; - 双缓冲乒乓:通过 SRV/UAV 角色互换避免读写冲突,无需为每帧分配新缓冲;
- 特性协商与降级:Root Signature 版本探测回退、Enhanced Barriers 探测回退两套降级路径,展示了面向不同硬件能力编写"渐进增强"代码的标准做法。
如果你希望进一步扩展,可将ThreadCount调大观察多计算上下文的视口分割效果,或调整 nBodyGravityCS.hlsl 中的软化项、引力常数与cbCS的时间步长/阻尼参数,观察不同物理行为下的模拟形态。
- 示例工程
【免费下载链接】DirectX-Graphics-Samples
This repo contains the DirectX Graphics samples that demonstrate how to build graphics intensive applications on Windows.
相关推荐
如何快速上手C Interfaces and Implementations:新手必备入门教程
如何快速上手C Interfaces and Implementations:新手必备入门教程 C Interfaces and Implementations
开发工具DirectX 12保留资源技术:基于DirectX-Graphics-Samples的高级特性
DirectX 12保留资源技术:基于DirectX Graphics Samples的高级特性 DirectX 12保留资源技术是现代图形编程中的高级特性,能
示例工程DirectX 12管线状态缓存:基于DirectX-Graphics-Samples的性能优化终极指南
DirectX 12管线状态缓存:基于DirectX Graphics Samples的性能优化终极指南 在现代图形应用开发中,DirectX 12管线状态缓存
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考