Skip to the content.
Posts

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:

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);