Introduction to Direct3D 12 Programming with GeeXLab



Since GeeXLab version 0.9+, a Direct3D 12 renderer plugin is available in the Windows versions (32 and 64-bit).

A complete and efficient implementation of a Direct3D 12 based renderer is a big and tough task. The current D3D12 plugin shipped with GeeXLab does not provide a full support (for example the management of several GPUs is not yet implemented in the plugin) but most of the basic functionalities is available: command lists (CL), pipeline state objects (PSO), constant buffers (CB) and HLSL shaders.

In this introduction, we’re going to see how to draw the hello world of graphics programming: a simple RGB triangle.


GeeXLab - Direct3D 12 demo - RGB Triangle

The full GeeXLab demo (02-triangle-of-death.xml) is available in the host_api/Direct3D12/ folder of the code sample pack.

 
This example is simple but at the same time, CL, PSO and HLSL shaders are all utilized.

The principle of Direct3D 12 is quite simple: the render calls are stored in a command list. Once all render call are stored, the command list is executed.

The first thing to do is to tell GeeXLab that the demo will use a D3D12 renderer instead of the default OpenGL renderer. This is done in the XML window node with the renderer_type attribute:

  <window name="win3d01" title="Direct3D 12 demo" 
          width="800" height="400" 
          renderer_type="Direct3D12" />

 
Now we can script (currently, the functions required by D3D12 are only available in Lua, I will update the Python plugin later). Let’s see the content of the INIT script.

The first step is to create a command list and open it:

cl = gh_renderer.command_list_create()
gh_renderer.command_list_open(cl)

 
Now all usual GeeXLab functions will fill the command list. For example, the initialization of the mesh triangle will fill the current command list in an implicit way:

triangle = gh_mesh.create_v2()
local num_vertices = 3
local num_faces = 0 -- non-indexed rendering
local ret = gh_mesh.alloc_mesh_data(triangle, num_vertices, num_faces)
if (ret == 1) then
  gh_mesh.set_vertex_position(triangle, 0, -2, -2, 0, 1)
  gh_mesh.set_vertex_position(triangle, 1, 0, 2, 0, 1)
  gh_mesh.set_vertex_position(triangle, 2, 2, -2, 0, 1)

  gh_mesh.set_vertex_color(triangle, 0, 1, 0, 0, 1) -- red
  gh_mesh.set_vertex_color(triangle, 1, 0, 1, 0, 1) -- green
  gh_mesh.set_vertex_color(triangle, 2, 0, 0, 1, 1) -- blue
end 

 
Direct3D 12 introduced a new object in order to optimize rendering: the Pipeline State Object or PSO. In short, a PSO describes a whole GPU context required for a particular type of rendering. The depth state, blending state, fill mode (solid, wireframe) are few states included in a PSO. A PSO includes a GPU program as well. A PSO is an immutable object: once built, it can not be updated… You follow me? Yes, your big D3D12 demo will have a lot of PSOs…

The creation and initialization of a PSO suited for our needs can be done with:

local RENDERER_POLYGON_MODE_POINT = 0
local RENDERER_POLYGON_MODE_LINE = 1
local RENDERER_POLYGON_MODE_SOLID = 2
local PRIMITIVE_TRIANGLE = 0
local PRIMITIVE_LINE = 2
local PRIMITIVE_POINT = 8

pso01 = gh_renderer.pipeline_state_create("pso01", vertex_color_prog, "")
gh_renderer.pipeline_state_set_attrib_4i(pso01, "DEPTH_TEST", 1, 0, 0, 0)
gh_renderer.pipeline_state_set_attrib_4i(pso01, "FILL_MODE", RENDERER_POLYGON_MODE_SOLID, 0, 0, 0)
gh_renderer.pipeline_state_set_attrib_4i(pso01, "PRIMITIVE_TYPE", PRIMITIVE_TRIANGLE, 0, 0, 0)
local ret = gh_renderer.pipeline_state_build(pso01)
if (ret == 0) then
	print("ERROR: pipeline state pso01 is not valid.")
end

 
Once the scene objects have been all initialized, we can close and perform a first execution of the command list in order to really create all GPU objects:

gh_renderer.command_list_close(cl)
gh_renderer.command_list_execute(cl)
gh_renderer.wait_for_gpu()

 
gh_renderer.wait_for_gpu() allows to synchronize the GPU and CPU operations.

Now we can detail the content of a FRAME script that will render our nice RGB triangle:

-- Reset the command list
gh_renderer.command_list_frame_begin(cl)

-- The draw calls will be rendered in the default framebuffer.
gh_renderer.d3d12_bind_framebuffer(cl)

-- Bind the camera and clear the color buffer.
gh_camera.bind(camera)
gh_renderer.clear_color_depth_buffers(0.2, 0.2, 0.2, 1.0, 1.0)

-- Bind the PSO
gh_renderer.pipeline_state_bind(pso01)

-- Render the triangle
gh_object.render(triangle)

-- Close the command list and execute it.
gh_renderer.command_list_frame_end(cl)

As I said, all render states and GPU program (in HLSL) are embedded in a PSO. So to render the triangle, we just need to bind the PSO and draw the triangle:

gh_renderer.pipeline_state_bind(pso01)
gh_object.render(triangle)

 
To end up the article, here is the HLSL program used to draw the triangle:

// ModelViewTransforms constant buffer uses b0 shader register in space register 1 
// (defined in GeeXLab).
//
cbuffer ModelViewTransforms : register(b0,space1) 

{
  column_major float4x4 ModelViewProjectionMatrix : packoffset(c0); 
  column_major float4x4 ModelViewMatrix : packoffset(c4);
  column_major float4x4 ModelMatrix : packoffset(c8);
  column_major float4x4 ViewMatrix : packoffset(c12);
  column_major float4x4 ProjectionMatrix : packoffset(c16);
  column_major float4x4 ViewProjectionMatrix : packoffset(c20);
  //float4 Viewport : packoffset(c24);
};

struct VSInput
{
  float4  Position : POSITION;
  float4  TexCoord : TEXCOORD;
  float4  Normal   : NORMAL;
  float4  Color    : COLOR;
};

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

PSInput VSMain(VSInput input)
{
  PSInput result;
  result.position = mul(ModelViewProjectionMatrix, float4(input.Position.xyz, 1.0)); 
  result.color = input.Color;
  return result;
}

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

 
All transformation matrices are passed to the program via a constant buffer called ModelViewTransforms. This particular constant buffer is managed by GeeXLab so no need to worry about it. All you need to know is that this constant buffer is bound to the register b0 of the shader register space 1: register(b0,space1). This is important otherwise your HLSL program won’t be compiled.

More Direct3D 12 demos are available in the host_api/Direct3D12 folder of the code sample pack. Do not hesitate to hack them in order to understand how they work. And if you are stuck on a problem, the forum is there (EN) or there (FR).