Jorge
Jorge Autor de Aprende Unreal Engine 4.

DirectX 12: Pipeline State

DirectX 12: Pipeline State

Para que podamos pintar algo por pantalla necesitamos configurar el pipeline. En este tutorial veremos como compilar los shaders y cambiar los parámetros del pipeline.

Tabla de contenidos

Parte 1. Window

Parte 2. Adapter

Parte 3. Conceptos

Parte 4. Queue

Parte 5. Resources

Parte 6. Descriptores

» Parte 7. Pipeline State

Parte 8. Swapchain

Parte 9. Triángulo

Parte 10. Root Signature

Parte 11. Texturas

Pipeline State

El pipeline en DX12 tiene la siguiente pinta:

DirectX Pipeline

Fuente: https://docs.microsoft.com/es-es/windows/win32/direct3d12/images/pipeline.png

Configurar el pipeline se refiere a:

  1. Subir los shaders a las etapas programables
  2. Setear los parámetros de cada etapa fija

El pipeline está encapsulado en la clase ID3D12PipelineState.

Para crear un ID3D12PipelineState se usa la estructura D3D12_GRAPHICS_PIPELINE_STATE_DESC.

ComPtr<ID3D12PipelineState> PipelineState;

D3D12_GRAPHICS_PIPELINE_STATE_DESC PSODesc{};
PSODesc.InputLayout = /* estructura con los parámetros de la etapa Input Assembler */;
PSODesc.VS = /* shader para vertex shader */;
PSODesc.PS = /* shader para pixel shader */;
PSODesc.RasterizerState = /* estructura con los parámetros de la etapa rasterizer */;
PSODesc.BlendState = /* como mezclar colores con transparencias */;
PSODesc.DepthStencilState = /* depth & stencil */;
PSODesc.SampleMask = UINT_MAX; /* The sample mask for the blend state */
PSODesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
PSODesc.NumRenderTargets = 1;
PSODesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
PSODesc.SampleDesc.Count = 1;

Device->CreateGraphicsPipelineState(&PSODesc, IID_PPV_ARGS(&PipelineState));

El objeto ID3D12PipelineState se usa cuando empiezas a grabar comandos, indicando qué estado debe tener el pipeline cuando ejecute los comandos:

ComPtr<ID3D12CommandAllocator> CommandAllocator;
ComPtr<ID3D12GraphicsCommandList> CommandList;
// ...

ComPtr<ID3D12PipelineState> PipelineState;
// ...

CommandAllocator->Reset();
CommandList->Reset(CommandAllocator.Get(), PipelineState.Get());
// ... grabar comandos ...
CommandList->Close();

También se puede cambiar el estado del pipeline explícitamente en cualquier punto de la ejecución:

CommandList->SetPipelineState(PipelineState.Get());

Vamos a ver como configurar el pipeline.

Input Layout

Tenemos el siguiente buffer de vértices:

struct Vertex
{
  XMFLOAT3 Position;
  XMFLOAT4 Color;
  XMFLOAT2 UV0;
  XMFLOAT2 UV1;
};

Vertex Vertices[] = {
  // { POS, COLOR, UV0, UV1 }
  { {-0.5f, -0.5f, 0.0f}, {1.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f}, {0.0f, 0.0f} },
  { {0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f, 1.0f}, {1.0f, 0.0f}, {1.0f, 0.0f} },
  { {0.0f, 0.5f, 0.0f}, {0.0f, 0.0f, 1.0f, 1.0f}, {0.5f, 1.0f}, {0.0f, 0.1f} }
};

ComPtr<ID3D12Resource> VertexBuffer;
// Crear vertex buffer. (Lo vimos en tutoriales anteriores)
// Copiar Vertices a este recurso. (Lo vimos en tutoriales anteriores)

D3D12_VERTEX_BUFFER_VIEW VertexBufferView;
// Descriptor de VertexBuffer

// ...

// Entrada de InputAssembler con este vertex buffer
CommandList->IASetVertexBuffers(0, 1, &VertexBufferView);

La primera etapa del pipeline, InputAssembler, debe tomar el vertex buffer y pasarle cada uno de los vértices a las siguientes etapas.

Sin ir más lejos, a la etapa inmediatamente siguiente: el Vertex Shader

// vertex shader (HLSL)
float4 VSMain(float3 position: POSITION, float4 color: COLOR, float2 uv0: UV0, float2 uv1: UV1) : SV_Position 
{
// ...
}

Sin embargo, el InputAssembler solo ve un vertex buffer que es un buffer contiguo de datos. ¿Cómo sabe que cada vértice ocupa X bytes y, de este modo, poder dividir el buffer en los distintos vértices? ¿Cómo sabe que dentro de cada vértice los primeros 12 bytes corresponden a la posición (4 bytes x 3 floats), los 16 siguientes corresponden al color, etc.,?

Y es más, cada aplicación tendrá su propia estructura Vertex diferente (algunos tendrán un campo para Normal, para coordenadas UV, etc.,).

Hay que indicarle el formato de algún modo al input assembler. Se hace rellenando una estructura de tipo D3D12_INPUT_LAYOUT_DESC.

D3D12_INPUT_ELEMENT_DESC pInputElementDescs[] = {
  // SemanticName; SemanticIndex; Format; InputSlot; AlignedByteOffset; InputSlotClass; InstanceDataStepRate;
  {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
  {"COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 3 * 4, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
  {"UV0", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 3 * 4 + 4 * 4, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
  {"UV1", 1, DXGI_FORMAT_R32G32_FLOAT, 0, 3 * 4 + 4 * 4 + 2 * 2, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0}    
};

D3D12_INPUT_LAYOUT_DESC InputLayout{};
InputLayout.NumElements = 4;
InputLayout.pInputElementDescs = pInputElementDescs;

D3D12_GRAPHICS_PIPELINE_STATE_DESC PSODesc{};
PSODesc.InputLayout = InputLayout;
/* ... */

Device->CreateGraphicsPipelineState(&PSODesc, IID_PPV_ARGS(&PipelineState));

Shaders

Para las etapas programables hay que compilar un shader.

El resultado de la compilación será un bloque de código representado por la clase ID3Blob:

ComPtr<ID3DBlob> ShaderBlob = LoadShader(...); // definimos esta función enseguida

Partiendo de este bloque de código creamos una referencia al shader:

D3D12_SHADER_BYTECODE ShaderBytecode;
ShaderBytecode.pShaderBytecode = ShaderBlob->GetBufferPointer();
ShaderBytecode.BytecodeLength = ShaderBlob->GetBufferSize();

// usar ShaderBytecode allí dónde necesite una referencia al shader

Podemos compilar un shader offline usando el compilador que trae de seria Visual Studio. También podemos compilarlo en runtime usando la función D3DCompileFromFile:

ComPtr<ID3DBlob> DemoApp::LoadShader(LPCWSTR Filename, LPCSTR EntryPoint, LPCSTR Target)
{
  HRESULT hr;
	
  ComPtr<ID3DBlob> ShaderBlob; // chunk of memory

  hr = D3DCompileFromFile(
    Filename, // FileName,
    nullptr, nullptr, // MacroDefines, Includes, 		
    EntryPoint, // FunctionEntryPoint,
    Target, // Target: "vs_5_0", "ps_5_0", "vs_5_1", "ps_5_1"
    D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION, // Compile flags
    0, // Flags2
    &ShaderBlob, // Code
    nullptr // Error
  );

  if (FAILED(hr))
  {
    OutputDebugString("[ERROR] D3DCompileFromFile -- Vertex shader");
  }

  return ShaderBlob;
}

Vertex Shader & Pixel Shader

Un fichero de shaders de ejemplo:

// shaders.hlsl

struct PSInput
{
    float4 position : SV_POSITION;
    float4 color : COLOR;
};

PSInput VSMain(float4 position : POSITION, float4 color : COLOR)
{
    PSInput result;
    
    result.position = position;
    result.color = color;

    return result;
}

float4 PSMain(PSInput input) : SV_TARGET
{
    return input.color;
}

Cargar los vertex y pixel shaders:

/* Shaders */

ComPtr<ID3DBlob> VertexBlob = LoadShader(L"shaders.hlsl", "VSMain", "vs_5_1");

D3D12_SHADER_BYTECODE VertexShaderBytecode;
VertexShaderBytecode.pShaderBytecode = VertexBlob->GetBufferPointer();
VertexShaderBytecode.BytecodeLength = VertexBlob->GetBufferSize();

ComPtr<ID3DBlob> PixelBlob = LoadShader(L"shaders.hlsl", "PSMain", "ps_5_1");

D3D12_SHADER_BYTECODE PixelShaderBytecode;
PixelShaderBytecode.pShaderBytecode = PixelBlob->GetBufferPointer();
PixelShaderBytecode.BytecodeLength = PixelBlob->GetBufferSize();

Y para pipeline state:

D3D12_GRAPHICS_PIPELINE_STATE_DESC PSODesc{};
// ...
PSODesc.VS = VertexShaderBytecode;
PSODesc.PS = PixelShaderBytecode;
// ...

Rasterizer Stage

La etapa de rasterización toma la representación gráfica 3D y genera los fragmentos que luego serán “coloreados” por el pixel shader.

D3D12_RASTERIZER_DESC RasterizerState{};
RasterizerState.FillMode = D3D12_FILL_MODE_SOLID;
RasterizerState.CullMode = D3D12_CULL_MODE_BACK;
RasterizerState.FrontCounterClockwise = FALSE;
RasterizerState.DepthBias = D3D12_DEFAULT_DEPTH_BIAS;
RasterizerState.DepthBiasClamp = D3D12_DEFAULT_DEPTH_BIAS_CLAMP;
RasterizerState.SlopeScaledDepthBias = D3D12_DEFAULT_SLOPE_SCALED_DEPTH_BIAS;
RasterizerState.DepthClipEnable = TRUE;
RasterizerState.MultisampleEnable = FALSE;
RasterizerState.AntialiasedLineEnable = FALSE;
RasterizerState.ForcedSampleCount = 0;
RasterizerState.ConservativeRaster = D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF;


D3D12_GRAPHICS_PIPELINE_STATE_DESC PSODesc{};
// ...
PSODesc.RasterizerState = RasterizerState;
// ...

Los campos son bastante autodescriptivos. Aquí la documentación oficial.

Depth & Stencil

Para el proyecto de ejemplo no usaremos depth/stencil:

/* Depth Stencil */

D3D12_DEPTH_STENCIL_DESC DepthStencilState{};
DepthStencilState.DepthEnable = FALSE;
DepthStencilState.StencilEnable = FALSE;

En futuros tutoriales lo veremos.

Blend State

La fase de blend en el pipeline ocurre en la etapa Output Merger.

¿Qué es blending? Una vez renderizado los fragmentos en la etapa Rasterizer y “coloreados” en la etapa Pixel Shader tenemos que decidir que color final tendra el pixel.

Un pixel puede contener varios fragmentos, cada uno con un color, ¿cómo los mezclamos?

La estructura D3D12_BLEND_DESC contiene la respuesta.

D3D12_BLEND_DESC BlendState{};
// ... rellenar la estructura ...

D3D12_GRAPHICS_PIPELINE_STATE_DESC PSODesc{};
// ...
PSODesc.BlendState = BlendState;
// ...

La estructura tiene la siguiente declaración:

typedef struct D3D12_BLEND_DESC {
  BOOL                           AlphaToCoverageEnable;
  BOOL                           IndependentBlendEnable;
  D3D12_RENDER_TARGET_BLEND_DESC RenderTarget[8];
} D3D12_BLEND_DESC;

El primer campo de la estructura indica si queremos AlphaToCoverage como una técnica de multisampling. El segundo si solo queremos hacer blend en el primer render target (recuerda que output merger tiene hasta hasta 8 render targets como salida). En general pondras ambos a FALSE, más información en la documentación oficial.

El último campo, y más importante, es un array de D3D12_RENDER_TARGET_BLEND_DESC indicando que operación de blend hacer en cada render target. El primer elemento del array describe la operación de blend para el primer render target, el segundo para el segundo y así sucesivamente.

typedef struct D3D12_RENDER_TARGET_BLEND_DESC {
  BOOL           BlendEnable;
  BOOL           LogicOpEnable;
  D3D12_BLEND    SrcBlend;
  D3D12_BLEND    DestBlend;
  D3D12_BLEND_OP BlendOp;
  D3D12_BLEND    SrcBlendAlpha;
  D3D12_BLEND    DestBlendAlpha;
  D3D12_BLEND_OP BlendOpAlpha;
  D3D12_LOGIC_OP LogicOp;
  UINT8          RenderTargetWriteMask;
} D3D12_RENDER_TARGET_BLEND_DESC;

Básicamente tenemos dos colores y queremos mezclarlos siguiendo la siguiente combinación lineal (para el valor del color y para el alpha):

Esto es precisamente lo que describe la estructura D3D12_RENDER_TARGET_BLEND_DESC los parámetros y así como la operación (suma, resta, etc.,).

En nuestro caso no queremos usar blend así que vamos a desactivarlo. Desgraciadamente tenemos que ser explícitos y rellenar todos los campos:

D3D12_BLEND_DESC BlendState{};
BlendState.AlphaToCoverageEnable = FALSE;
BlendState.IndependentBlendEnable = FALSE;
const D3D12_RENDER_TARGET_BLEND_DESC DefaultRenderTargetBlendDesc =
{
  FALSE,FALSE,
  D3D12_BLEND_ONE, D3D12_BLEND_ZERO, D3D12_BLEND_OP_ADD,
  D3D12_BLEND_ONE, D3D12_BLEND_ZERO, D3D12_BLEND_OP_ADD,
  D3D12_LOGIC_OP_NOOP,
  D3D12_COLOR_WRITE_ENABLE_ALL,
};
for (UINT i = 0; i < D3D12_SIMULTANEOUS_RENDER_TARGET_COUNT; ++i)
{
  BlendState.RenderTarget[i] = DefaultRenderTargetBlendDesc;
} 

Root Signature

En tutoriales anteriores hemos visto como crear un recurso constant buffer y un descriptor para el mismo.

struct FSomeConstants {
  XMMATRIX MVP;
  XMFLOAT3 LightDir;
};

FSomeConstants SomeConstants;
SomeConstants.MVP = ...;
SomeConstants.LightPos = ...;

// En anterior tutoriales vimos como crear un recurso y obtener un descriptor:

ComPtr<ID3D12DescriptorHeap> ConstantBufferDescriptorHeap;
//...
D3D12_CPU_DESCRIPTOR_HANDLE ConstantBufferDescriptor = ConstantBufferDescriptorHeap->GetCPUDescriptorHandleForHeapStart();

¿Cómo pasamos dicho descriptor al shader?

float4x4 MVP;  // <-- deberiamos usar el descriptor ConstantBufferDescriptor aquí
float3 LightDir; // <-- y aquí también

float4 VSMain(float4 pos: POSITION, float4: COLOR): SV_POSITION
{
// ...
}

float4 PSMain(float pos: SV_Position): SV_TARGET
{
// ...
}

La clave está en indicar en el shader que esos valores están alojados en un constant buffer y además indicar en qué registro está el descriptor:

cbuffer SomeConstants : register(b0)
{
  float4x4 MVP;
  float3 LightDir;
}

float4 VSMain(float4 pos: POSITION, float4: COLOR): SV_POSITION
{
// ...
}

float4 PSMain(float pos: SV_Position): SV_TARGET
{
// ...
}

Desde el lado del shader hemos dicho que esas constantes (MVP y LightDir) están alojadas en un buffer constante y que además el descriptor que describe dicho buffer será cargado en el registro b0.

Nos queda, desde el lado del código, completar el mapeo. Desde el lado de la aplicación tenemos que indicar que el descriptor ConstantBufferDescriptor tiene que ser cargado en el registro b0.

¿Por qué el registro b0? Los registros b0, b1, … bN se usan para constantes. En este ejemplo podríamos haber usado cualquier registro bX. Los registros t0, t1, … tN se usan para texturas y los registros s0, s1, … sN para samplers.

Para completar el mapeo desde el lado del código se usa RootSignature.

RootSignature indica que descriptores van a qué registros.

D3D12_ROOT_SIGNATURE_DESC RootSignatureDesc{};

RootSignatureDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;
// el InputAssembler va a tomar como entrada un vertex buffer

RootSignatureDesc.NumParameters = 0;

RootSignatureDesc.pParameters = nullptr; 
// en este campo iría un array de parámetros.
// cara parámetro empareja un descriptor con un registro
// en nuestro ejemplo uno de los parámetros debería indicar 
//     ConstantBufferDescriptor <=> b0

RootSignatureDesc.NumStaticSamplers = 0;
RootSignatureDesc.pStaticSamplers = nullptr;	

ComPtr<ID3DBlob> Signature; // chunk of memory
ComPtr<ID3DBlob> Error; // chunk of memory

D3D12SerializeRootSignature(&RootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &Signature, &Error);

ComPtr<ID3D12RootSignature> RootSignature;
Device->CreateRootSignature(0, Signature->GetBufferPointer(), Signature->GetBufferSize(), IID_PPV_ARGS(&RootSignature));

Ahora mismo no vamos a pasarle ningún recurso al shader: ni texturas, ni constant buffers, ni nada. Así que nos vale con un root signature vacío tal y como está escrito en el código anterior.

En próximos tutoriales añadiremos parámetros al root signature.

Veremos más adelante el código concreto para añadir parámetros en el root signature.

Código para el proyecto

Añade el siguiente código al proyecto demo:

// DemoApp.h
class DemoApp
{
    // ...
private:
    
  /* Pipeline */
  ComPtr<ID3DBlob> LoadShader(LPCWSTR Filename, LPCSTR EntryPoint, LPCSTR Target);
  ComPtr<ID3D12RootSignature> RootSignature;
  ComPtr<ID3D12PipelineState> PipelineState;
  void CreateRootSignature();
  void CreatePipeline();
    
};
// DemoApp.cpp

void DemoApp::CreateRootSignature()
{
  D3D12_ROOT_SIGNATURE_DESC SignatureDesc{};
  SignatureDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;	
  SignatureDesc.NumParameters = 0;
  SignatureDesc.NumStaticSamplers = 0;

  ComPtr<ID3DBlob> Signature;
  ComPtr<ID3DBlob> Error;

  D3D12SerializeRootSignature(&SignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1_0, &Signature, &Error);
  Device->CreateRootSignature(0, Signature->GetBufferPointer(), Signature->GetBufferSize(), IID_PPV_ARGS(&RootSignature));
}

void DemoApp::CreatePipeline()
{
  /* Shaders */

  ComPtr<ID3DBlob> VertexBlob = LoadShader(L"shaders.hlsl", "VSMain", "vs_5_1");

  D3D12_SHADER_BYTECODE VertexShaderBytecode;
  VertexShaderBytecode.pShaderBytecode = VertexBlob->GetBufferPointer();
  VertexShaderBytecode.BytecodeLength = VertexBlob->GetBufferSize();

  ComPtr<ID3DBlob> PixelBlob = LoadShader(L"shaders.hlsl", "PSMain", "ps_5_1");

  D3D12_SHADER_BYTECODE PixelShaderBytecode;
  PixelShaderBytecode.pShaderBytecode = PixelBlob->GetBufferPointer();
  PixelShaderBytecode.BytecodeLength = PixelBlob->GetBufferSize();

  /* Input Layout */

  D3D12_INPUT_ELEMENT_DESC pInputElementDescs[] = {
    // SemanticName; SemanticIndex; Format; InputSlot; AlignedByteOffset; InputSlotClass; InstanceDataStepRate;
    {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
    {"COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 3 * 4, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0}
  };

  D3D12_INPUT_LAYOUT_DESC InputLayout{};
  InputLayout.NumElements = 2;
  InputLayout.pInputElementDescs = pInputElementDescs;

  /* Rasterizer Stage */

  D3D12_RASTERIZER_DESC RasterizerState{};
  RasterizerState.FillMode = D3D12_FILL_MODE_SOLID;
  RasterizerState.CullMode = D3D12_CULL_MODE_BACK;
  RasterizerState.FrontCounterClockwise = FALSE;
  RasterizerState.DepthBias = D3D12_DEFAULT_DEPTH_BIAS;
  RasterizerState.DepthBiasClamp = D3D12_DEFAULT_DEPTH_BIAS_CLAMP;
  RasterizerState.SlopeScaledDepthBias = D3D12_DEFAULT_SLOPE_SCALED_DEPTH_BIAS;
  RasterizerState.DepthClipEnable = TRUE;
  RasterizerState.MultisampleEnable = FALSE;
  RasterizerState.AntialiasedLineEnable = FALSE;
  RasterizerState.ForcedSampleCount = 0;
  RasterizerState.ConservativeRaster = D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF;

  /* Blend State */

  D3D12_BLEND_DESC BlendState{};
  BlendState.AlphaToCoverageEnable = FALSE;
  BlendState.IndependentBlendEnable = FALSE;
  const D3D12_RENDER_TARGET_BLEND_DESC DefaultRenderTargetBlendDesc =
  {
    FALSE,FALSE,
    D3D12_BLEND_ONE, D3D12_BLEND_ZERO, D3D12_BLEND_OP_ADD,
    D3D12_BLEND_ONE, D3D12_BLEND_ZERO, D3D12_BLEND_OP_ADD,
    D3D12_LOGIC_OP_NOOP,
    D3D12_COLOR_WRITE_ENABLE_ALL,
  };
  for (UINT i = 0; i < D3D12_SIMULTANEOUS_RENDER_TARGET_COUNT; ++i)
  {
    BlendState.RenderTarget[i] = DefaultRenderTargetBlendDesc;
  } 
	
  /* Depth Stencil */

  D3D12_DEPTH_STENCIL_DESC DepthStencilState{};
  DepthStencilState.DepthEnable = FALSE;
  DepthStencilState.StencilEnable = FALSE;

  /* Pipeline */

  D3D12_GRAPHICS_PIPELINE_STATE_DESC PSODesc{};
  PSODesc.InputLayout = InputLayout;
  PSODesc.pRootSignature = RootSignature.Get();
  PSODesc.VS = VertexShaderBytecode;
  PSODesc.PS = PixelShaderBytecode;
  PSODesc.RasterizerState = RasterizerState;
  PSODesc.BlendState = BlendState;
  PSODesc.DepthStencilState = DepthStencilState;
  PSODesc.SampleMask = UINT_MAX;
  PSODesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
  PSODesc.NumRenderTargets = 1;
  PSODesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
  PSODesc.SampleDesc.Count = 1;

  Device->CreateGraphicsPipelineState(&PSODesc, IID_PPV_ARGS(&PipelineState));
}

El constructor quedaría así:

DemoApp::DemoApp()
{	
  CreateDevice();
  CreateQueues();
  CreateFence();

  //CreateSwapchain(hWnd, Width, Height);
  CreateRenderTargets();

  CreateRootSignature();
  CreatePipeline();
}

comments powered by Disqus