Recently, I wanted to mess around with mesh shaders in D3D12 but I couldn’t easily find the Hello, world!
example of one. I pieced one together thanks to these resources:
- BeastLe9enD’s comment on this issue
- SaschaWillem’s HLSL Vulkan mesh shading sample
- Sarah Jobalia’s DirectX Developer Blog article on Mesh Shaders and Amplification Shaders
Yes, I know I’m a bit late to the game, but better late than never.
Source Code
GREX Project: 110_mesh_shader_triangle_d3d12
C++ Source: 110_mesh_shader_triangle_d3d12.cpp
Configuring Headers
I didn’t have necessary headers on my system so I used the headers from DirectX-Headers.
Since my helper code also gets used in other projects, I needed to keep it easy for projects to decide which set of headers to use:
#if defined(GREX_USE_D3DX12)
#include "directx/d3dx12.h"
#else
#include <d3d12.h>
#endif
I limited the include path to DirectX-Headers/include
so I can use the directx
sub-directory to qualify which set of D3D12 headers are included. And of course the quotes vs brackets also help!
Mesh Shader
Compiled using ms_6_5
profile. For this example, MeshOutput
uses the same semantics that would appear in a typical VSOutput
struct.
struct MeshOutput {
float4 Position : SV_POSITION;
float3 Color : COLOR;
};
[outputtopology("triangle")]
[numthreads(1, 1, 1)]
void msmain(out indices uint3 triangles[1], out vertices MeshOutput vertices[3])
{
SetMeshOutputCounts(3, 1);
triangles[0] = uint3(0, 1, 2);
vertices[0].Position = float4(-0.5, 0.5, 0.0, 1.0);
vertices[0].Color = float3(1.0, 0.0, 0.0);
vertices[1].Position = float4(0.5, 0.5, 0.0, 1.0);
vertices[1].Color = float3(0.0, 1.0, 0.0);
vertices[2].Position = float4(0.0, -0.5, 0.0, 1.0);
vertices[2].Color = float3(0.0, 0.0, 1.0);
}
Pixel Shader
Compiled using ps_6_5
profile to match the mesh shader for completeness. No surprises here.
float4 psmain(MeshOutput input) : SV_TARGET
{
return float4(input.Color, 1);
}
Pipeline State Object
I was pleasantly surprised that pipeline creation for mesh shader based pipelines are a lot simpler than traditional vertex-hull-domain-geometry based pipelines.
The only thing that caught me a bit off guard was the usages of a CD3DX12
helper struct as part of the pipeline creation. I haven’t had the occasion to use the helper structs, but this seemed like a good time. It looks like you can roll your own structs for handling the state stream stuff but that seemed like unnecessary work for this simple example. See DirectX-Specs for Mesh Shader for more details.
D3DX12_MESH_SHADER_PIPELINE_STATE_DESC psoDesc = {};
psoDesc.pRootSignature = rootSig.Get();
psoDesc.MS = {dxilMS.data(), dxilMS.size()};
psoDesc.PS = {dxilPS.data(), dxilPS.size()};
psoDesc.BlendState.AlphaToCoverageEnable = FALSE;
psoDesc.BlendState.IndependentBlendEnable = FALSE;
psoDesc.BlendState.RenderTarget[0].BlendEnable = FALSE;
psoDesc.BlendState.RenderTarget[0].LogicOpEnable = FALSE;
psoDesc.BlendState.RenderTarget[0].SrcBlend = D3D12_BLEND_SRC_COLOR;
psoDesc.BlendState.RenderTarget[0].DestBlend = D3D12_BLEND_ZERO;
psoDesc.BlendState.RenderTarget[0].BlendOp = D3D12_BLEND_OP_ADD;
psoDesc.BlendState.RenderTarget[0].SrcBlendAlpha = D3D12_BLEND_SRC_ALPHA;
psoDesc.BlendState.RenderTarget[0].DestBlendAlpha = D3D12_BLEND_ZERO;
psoDesc.BlendState.RenderTarget[0].BlendOpAlpha = D3D12_BLEND_OP_ADD;
psoDesc.BlendState.RenderTarget[0].LogicOp = D3D12_LOGIC_OP_NOOP;
psoDesc.BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
psoDesc.SampleMask = D3D12_DEFAULT_SAMPLE_MASK;
psoDesc.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID;
psoDesc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
psoDesc.RasterizerState.FrontCounterClockwise = TRUE;
psoDesc.RasterizerState.DepthBias = D3D12_DEFAULT_DEPTH_BIAS;
psoDesc.RasterizerState.DepthBiasClamp = D3D12_DEFAULT_DEPTH_BIAS_CLAMP;
psoDesc.RasterizerState.SlopeScaledDepthBias = D3D12_DEFAULT_SLOPE_SCALED_DEPTH_BIAS;
psoDesc.RasterizerState.DepthClipEnable = FALSE;
psoDesc.RasterizerState.MultisampleEnable = FALSE;
psoDesc.RasterizerState.AntialiasedLineEnable = FALSE;
psoDesc.RasterizerState.ForcedSampleCount = 0;
psoDesc.DepthStencilState.DepthEnable = FALSE;
psoDesc.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;
psoDesc.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_LESS_EQUAL;
psoDesc.DepthStencilState.StencilEnable = FALSE;
psoDesc.DepthStencilState.StencilReadMask = D3D12_DEFAULT_STENCIL_READ_MASK;
psoDesc.DepthStencilState.StencilWriteMask = D3D12_DEFAULT_STENCIL_WRITE_MASK;
psoDesc.DepthStencilState.FrontFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
psoDesc.DepthStencilState.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
psoDesc.DepthStencilState.FrontFace.StencilPassOp = D3D12_STENCIL_OP_KEEP;
psoDesc.DepthStencilState.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_NEVER;
psoDesc.DepthStencilState.BackFace = psoDesc.DepthStencilState.FrontFace;
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
psoDesc.NumRenderTargets = 1;
psoDesc.RTVFormats[0] = DXGI_FORMAT_B8G8R8A8_UNORM;
psoDesc.SampleDesc.Count = 1;
psoDesc.SampleDesc.Quality = 0;
CD3DX12_PIPELINE_MESH_STATE_STREAM psoStream
= CD3DX12_PIPELINE_MESH_STATE_STREAM(psoDesc);
D3D12_PIPELINE_STATE_STREAM_DESC steamDesc = {};
steamDesc.SizeInBytes = sizeof(psoStream);
steamDesc.pPipelineStateSubobjectStream = &psoStream;
ComPtr<ID3D12PipelineState> pipelineState;
HRESULT hr = renderer->Device->CreatePipelineState(
&steamDesc,
IID_PPV_ARGS(&pipelineState));
if (FAILED(hr))
{
assert(false && "Create pipeline state failed");
return EXIT_FAILURE;
}
CommandList Version
To keep things simple, I used DispatchMesh()
, which required ID3D12GraphicsCommandList6
.
renderer->Device
points to an ID3D12Device7
device, no particular reason - that’s just what’s in helper code.
ComPtr<ID3D12GraphicsCommandList6> commandList;
{
CHECK_CALL(renderer->Device->CreateCommandList1(
0, // nodeMask
D3D12_COMMAND_LIST_TYPE_DIRECT, // type
D3D12_COMMAND_LIST_FLAG_NONE, // flags
IID_PPV_ARGS(&commandList))); // ppCommandList
}
Dispatching Mesh Shader
Finally, to render using DispatchMesh()
, the command list recording looks something like:
commandList->OMSetRenderTargets(
1,
&renderer->SwapchainRTVDescriptorHandles[bufferIndex],
false,
&renderer->SwapchainDSVDescriptorHandles[bufferIndex]);
float clearColor[4] = {0.23f, 0.23f, 0.31f, 0};
commandList->ClearRenderTargetView(
renderer->SwapchainRTVDescriptorHandles[bufferIndex],
clearColor,
0,
nullptr);
D3D12_VIEWPORT viewport
= {0, 0, static_cast<float>(gWindowWidth), static_cast<float>(gWindowHeight), 0, 1};
commandList->RSSetViewports(1, &viewport);
D3D12_RECT scissor = {0, 0, static_cast<long>(gWindowWidth), static_cast<long>(gWindowHeight)};
commandList->RSSetScissorRects(1, &scissor);
commandList->SetPipelineState(pipelineState.Get());
commandList->DispatchMesh(1, 1, 1);