Compiling HLSL Shaders with Direct3D12 and CMake (With Error Reporting) Part 1
Note: This post is still in progress (don’t worry I am working on it almost everyday, I didn’t abandon it and it will be ready very soon). However, feel free to message me about improving the current draft if you have any ideas
While learning Direct3D12 and working on my game engine Mizu I found that there are many ways to compile shaders but there is no clear documentation about the different shader compilation ways and how to apply them (especially if you are using CMake as a build system). Therefore, as usual no clear documentation? Time to write a post on this blog
1. Getting the intial HelloTriangle project
As a start I will be using Microsoft’s Hello Triangle example from the D3D12HelloWorld example.
Visual Studio is needed of course (I am using 2022 version and 2015, 2017 should work for you as long as you have the suitable windows sdk and you can install the latest one from Visual Studio installer to be sure)
I will isolate the HelloTriangle example alone and use CMake as a build system for that standalone project.
You don’t have to do anything but I just wanted to explain how I got to the CMake file in the repository for the code of this post. Also, A true C++ programmer should be familiar with CMake :P
- Let’s take only the source and header files in HelloTriangle’s folder, put them in a folder called
HelloTriangle
(for example) and remove everything else. - Now we will create a
CMakeLists.txt
file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
project("Hello Triangle")
cmake_minimum_required(VERSION 3.21)
add_definitions(-D_UNICODE)
add_definitions(-DUNICODE)
link_libraries(
d3d12.lib
dxguid.lib
DXGI.lib
d3dcompiler.lib
)
set(HelloTriangle1_SOURCES
D3D12HelloTriangle.cpp
DXSample.cpp
stdafx.cpp
Win32Application.cpp
)
set(HelloTriangle1_HEADERS
D3D12HelloTriangle.h
DXSample.h
DXSampleHelper.h
stdafx.h
Win32Application.h
)
set(HelloTriangle1_SHADERS
shaders.hlsl
)
set_source_files_properties(shaders.hlsl PROPERTIES VS_TOOL_OVERRIDE "None")
add_executable(HelloTriangle1 WIN32 Main.cpp ${HelloTriangle1_HEADERS} ${HelloTriangle1_SOURCES} ${HelloTriangle1_SHADERS})
target_include_directories(HelloTriangle1 PUBLIC ${CMAKE_CURRENT_LIST_DIR})
source_group("Shaders" FILES ${HelloTriangle1_SHADERS})
Explaination of the lines:
- 1 to 2: In any cmake file we should define a project name and the minimum version which the person who is building the project should have.
- 4 to 5: Because we use wide string aka
std::wstring
in C++ for Direct3D API, this definition should be added from CMake otherwise we will get errors because the project will not know if you should use ANSI or UNICODE so you should define which one is it for the project More info in this question on StackOverFlow. - 7 to 12: We should link to Direct3D libraries but here I just used
link_libraries
to link to all the libraries and executables because the project is already small and that won’t be an issue here. - 14 to 19: Just make a list
HelloTriangle1_SOURCES
which contains the names of thecpp
files. - 21 to 27: same as previous point but a list for headers instead.
- 29 to 31: Making a list for Shader HLSL files (because we wanna tell Visual Studio to include them and treat them in a special way)
- 33: Visual Studio usually tries to compile the HLSL files in the default way automatically. For example, if we have a Pixel Shader it will detect its type and the HLSL compiler will make sure it is fine and without syntax errors. However, in this case we have put all shaders in one file and the default behaviour will cause compilation errors and settings should be changed. You can change it manually, but in this case you either have to upload the sln file and other related files with it, or you can just add this option in CMake which will make the project settings not using this default behaviour by default and not calling the shader compiler. We will talk about the shader compiler later in this post.
- 35: We add an executable called
HelloTriangle1
with typeWIN32
because otherwise it will think it’s an console application by default and the way we write the main function in Direct3D applications will cause compiler errors and not work. Then, we add all our files from the 3 lists (there is no need usually to add headers but I just added everything all together with sources).
If you don’t add Shader files in the list of sources given to
add_executable
, they will not appear in the project in Visual Studio and it will be inconvinent to use another editor or window to modify their code.
- 37: We make the current
CMAKE_CURRENT_LIST_DIR
(where the current CMake file is) a directory visible to#include
so that we can import our headers in any source file. Again, since the project is small I made everything visible to#include
- 38:
source_group
is used to make Visual Studio put the shader files inside a special folder (Shaders
in this case) because it will be convinent for organization inside Visual Studio’s solution explorer
If you don’t add a
source_group
for shaders, they will be in the same directory level ofsource files
andheader files
in Visual Studio’s solution explorer which works of course but feels weird and not organized in an elegant way
Now there are 3 main ways we can use shaders in our Direct3D program:
- Using
D3DCompileFromFile
function - Let VS automatically do it for us
- Using the
DirectXShaderCompiler
We will talk about the first and second one here (I will reference them here as I write them)
if you check the file D3D12HelloTriangle.cpp
line number 163 and the one after it, you will see the following:
1
2
3
ThrowIfFailed(D3DCompileFromFile(GetAssetFullPath(L"../../shaders.hlsl").c_str(), nullptr, nullptr, "VSMain", "vs_5_0", compileFlags, 0, &vertexShader, nullptr));
ThrowIfFailed(D3DCompileFromFile(GetAssetFullPath(L"../../shaders.hlsl").c_str(), nullptr, nullptr, "PSMain", "ps_5_0", compileFlags, 0, &pixelShader, nullptr));
Here we use the function ThrowIfFailed
which throws and exception if the expression inside it returns a failure through its HLRESULT
return type. Inside it, we got D3DCompileFromFile
with the following signature:
1
2
3
4
5
6
7
8
9
10
11
12
13
HRESULT D3DCompile(
[in] LPCVOID pSrcData,
[in] SIZE_T SrcDataSize,
[in, optional] LPCSTR pSourceName,
[in, optional] const D3D_SHADER_MACRO *pDefines,
[in, optional] ID3DInclude *pInclude,
[in, optional] LPCSTR pEntrypoint,
[in] LPCSTR pTarget,
[in] UINT Flags1,
[in] UINT Flags2,
[out] ID3DBlob **ppCode,
[out, optional] ID3DBlob **ppErrorMsgs
);
Where:
[in] pFileName
: A pointer to a constant null-terminated string that contains the name of the file that contains the shader code. it takes a wide string literall (usually passed like this:L"content here"
).Here I used
GetAssetFullPath function and passed the relative path so that it will return the full path like C:/Junk/shader.hlsl because the compiler takes the full path only (seems project relative one is different from the compiler relative one so GetAssetFullPath results gets evaluated relative to project then it's passed full to compiler. Remember that the compiler is in program files with visual studio compiler somewhere so it's relative folders and files are totally somewhere else). Also, I used
.c_str()` in the end because it accepts a literal not a wstring variable so I passed the literal out of the variable.[in, optional] pDefines
: An optional array of D3D_SHADER_MACRO structures that define shader macros. Here we don’t need that so we passednullptr
(or you can passNULL
)[in, optional] pInclude
: An optional pointer to an ID3DInclude interface that the compiler uses to handle include files. Here we didn’t include any files so we passnullptr
too[in] pEntrypoint
: A pointer to a constant null-terminated string that contains the name of the main function of the shader (likeint main()
in C++ and C but here you don’t have to call itmain
just tell the compiler where is it)[in] pTarget
: A pointer to a constant null-terminated string that specifies the shader target or set of shader features to compile against. So here I passedvs_5_0
to first call to tell the function we wanna use version 5 and this is a vertex shader (vs) and same idea for second one to use the pixel shader (ps). You can check the list of suchcompiler targets
here[in] Flags1
: Shader compile options and we can use OR operator between many of them if we wanna use more than one. Full list of such options here. Here we usedD3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION
in case of debug and nothing in case of Release and stored them incompileFlags
variable.[in] Flags2
: Effect compile options (Effect options
here andCompiler options
in the ones before them). Full list here. They are not needed here so we passed 0.[out] ppCode
: A pointer to a variable that receives a pointer to theID3DBlob
interface that you can use to access the compiled code. So we passed here the reference toComPtr<ID3DBlob> vertexShader
which is&vertexShader
.
Don’t forget that you have for ComPtr: RelaseAndGetAddressOf()which is the same as & or you can use GetAddressOf() function if you don’t wanna release unlike what we did here because we wanna pass it to store the returned info so it does not matter
[out, optional] ppErrorMsgs
An optional pointer to a variable that receives a pointer to the ID3DBlob interface and we can use it to get the compiler error messages. We passednullptr
for now and will show later how it works. I advise you to put it as a good practice to detect errors and understand them easier.
To be continued…
References:
- D3DCompileFromFile function Microsoft docs <!— You can find the current project inside this folder in
Direct3DExamples
repository in this link
Hope everything is clear and if you have any feedback feel free to contact me on discord, comment here or email.
TODO add advantages and disadvantages of this way and finish the tutorial for now Thanks for reading ! –>