Creating a window
Before we can draw anything we need two things from the operating system: a window to draw in, and a connection to the GPU that can put pictures into that window. Neither is part of a graphics API's core. OpenGL deliberately leaves windows to the platform, which is why LearnOpenGL starts by installing GLFW, a small library that opens a window and creates an OpenGL context on any desktop system, and GLAD, which fetches OpenGL's functions from the driver.
We are on one platform only, and on Windows both jobs are short. Opening a window takes a handful of Win32 calls, and Direct3D 11 creates its own device and swap chain for that window in one function. So we use no windowing library at all, and each lesson writes that code out itself. The next chapter goes through it line by line.
What this chapter covers instead is the part LearnOpenGL spends on building GLFW and setting up a Visual Studio project: the tools that turn our source files into programs, how a lesson is laid out on disk, and how the programs find their shaders and images when they run.
The toolchain
Our programs are written in rin, a small C-like language whose compiler translates each program into one C file. From there an ordinary C toolchain takes over. Building a lesson therefore involves a few separate tools, each doing one job:
- The
rin compiler ,rin.exe, checks the rin source and writes C. It lives in a folder calledrin-windows-x64together with rin's standard library, thestdfolder, which is whereimport "std/print.rin"is found. Get it from the rin repository, and put the folder on yourPATHso thatrinruns from any directory. CMake describes a C build independently of any particular compiler or IDE and generates the actual build files from that description.Ninja is the small, fast build tool those files are generated for. It works out what has changed and runs only the commands needed.clang-cl is the C compiler: LLVM's Clang with the command-line interface of Microsoft's compiler, so it produces normal Windows executables and debug information that Windows debuggers read.- The
Windows SDK provides the headers and import libraries for everything we call:windows.h,d3d11.h,dxgi.h,d3dcompiler.handwincodec.h, and the.libfiles that go with them. It comes with Visual Studio or the free Build Tools for Visual Studio, which also supply the C runtime that clang-cl links against.
CMake, Ninja and clang-cl must all be on the PATH; LLVM's Windows installer and CMake's both offer to add themselves. clang-cl locates an installed Visual Studio's C runtime and Windows SDK by itself. If configuration ever complains that it cannot find standard headers or libraries, running the build from an "x64 Native Tools Command Prompt", which sets those paths explicitly, settles it.
d3dcompiler.dll, the image decoders of the Windows Imaging Component) or short rin files in the repository's includes/ folder.
Building the lessons
Get the lessons from GitHub, open a terminal in the repository's top folder and run
rin build
build\cmake\hello_window.exe
The first command builds every lesson. The second runs the first one, which should open a window filled with a dark teal colour. If it does, every tool is in place.
build.rin
rin build takes its instructions from a file called build.rin in the top folder. It is written in rin syntax, so the same parser and the same editor support handle it, but it is never run: the compiler only reads a few globals with known names out of it. Here it is, with most of the lesson list cut:
build_name: [*]const char = "rin-dx11";
build_dir: [*]const char = "build";
build_generator: [*]const char = "Ninja";
build_compiler: [*]const char = "clang-cl";
build_type: [*]const char = "Debug";
build_entries: [38][*]const char = {
"src/1.getting_started/1.1.hello_window/hello_window.rin",
"src/1.getting_started/2.1.hello_triangle/hello_triangle.rin",
...
"src/6.pbr/2.2.ibl_specular/ibl_specular.rin",
};
build_defines: [1][*]const char = { "_CRT_SECURE_NO_WARNINGS" };
build_libraries: [7][*]const char = {
"user32", "d3d11", "dxgi", "d3dcompiler", "dxguid", "ole32", "windowscodecs",
};
Each line is a setting. The build goes into the build folder, is generated for Ninja, compiled by clang-cl, and is a Debug build, with debug information and without optimisation. The build_entries list names the programs: thirty-eight of them, one per lesson, each a separate executable named after its file. Lessons share no code at link time; each one becomes a single C file that holds everything it uses.
What rin build does
For each entry, rin build runs the compiler on it and writes the C file to build\rin_gen\, so hello_window.rin becomes build\rin_gen\hello_window.c. It then writes a CMakeLists.txt beside those C files, with one executable target per lesson, and finally asks CMake to configure a Ninja build in build\cmake\ and run it. The executables land in build\cmake\, each with a .pdb file holding its debug information.
The generated C is readable, and worth a look once. A small procedure from the first lesson comes out like this:
#line 35 "src/1.getting_started/1.1.hello_window/hello_window.rin"
b32 key_down(i32 key) {
return (GetAsyncKeyState(key) < 0);
}
The #line directives tell the C compiler which rin line each piece of C came from. Because of them, compiler errors and debuggers refer to our .rin files rather than the generated C: open hello_window.exe in the Visual Studio debugger (or any debugger that reads .pdb files), and breakpoints and single-stepping work on the rin source.
The generated CMakeLists.txt is rewritten on every build, so there is no reason to edit it. To change the build, change build.rin.
Linking
The build_libraries list is the equivalent of the linker settings LearnOpenGL fills in for GLFW and opengl32.lib. Each name is a Windows SDK import library, the small .lib that tells the linker a function lives in a system DLL:
user32: windows, messages and keyboard state.d3d11anddxgi: Direct3D 11 and the swap chain.d3dcompiler:D3DCompile , which compiles our HLSL shaders while the program starts.dxguid: the interface GUIDs, such as IID_ID3D11Texture2D, that some calls need.ole32andwindowscodecs: COM start-up and the Windows Imaging Component, which decodes our.pngand.jpgfiles.
The single define, _CRT_SECURE_NO_WARNINGS, silences the C runtime's warnings about functions like
If you leave a library out, the rin compiler is perfectly happy, the C compiler is too, and the linker then fails with "unresolved external symbol" for each function from that library. That error almost always means a missing entry here.
The project layout
The repository follows the layout of LearnOpenGL's own code repository, so that the two can be read side by side:
build.rin how the lessons are built
src/
1.getting_started/
1.1.hello_window/
hello_window.rin the program
2.1.hello_triangle/
hello_triangle.rin the program
hello_triangle.hlsl its shaders: vs_main, ps_main
...
2.lighting/
...
includes/
win32.rin Win32, Direct3D 11, DXGI, D3DCompile and WIC declarations
math.rin vectors and matrices
image.rin .png and .jpg through WIC, Radiance .hdr
learn/
filesystem.rin paths under the repository root
shader.rin compile a .hlsl file, create its input layout
camera.rin the fly camera
model.rin Wavefront .obj models and their textures
resources/ textures, models, the HDR environment
Every lesson is one .rin file, read top to bottom: settings, window, device, shaders, data, render loop, in the same order as LearnOpenGL's C++ files. Its HLSL file sits beside it and holds all of its shaders, with entry points named vs_main and ps_main (and gs_main where there is a geometry shader).
The includes/ folder plays the part of LearnOpenGL's includes/. win32.rin stands in for the GLFW and GLAD headers, math.rin for GLM, and image.rin for stb_image. The learn/ folder holds small helpers in the spirit of LearnOpenGL's shader.h, camera.h, model.h and filesystem.h, each introduced in the chapter that first needs it. A lesson pulls these in with imports relative to its own file:
import "../../../includes/win32.rin"
import "../../../includes/learn/filesystem.rin"
import "std/print.rin"
Calling Windows from rin
LearnOpenGL needs GLAD because most OpenGL functions must be looked up in the driver while the program runs. We have no such step; Direct3D's functions are linked normally. What we do need is for rin to know what those functions look like, so that it can check our calls. That is win32.rin's job. It includes the real SDK headers into the generated C, then declares the parts we use:
cinclude "windows.h"
cinclude "d3d11.h"
...
HWND: alias[external] = *void;
...
D3DCompile: proc[external](source: *const void, size: usize, name: [*]const char, defines: *const D3D_SHADER_MACRO, include: *ID3DInclude, entry: [*]const char, target: [*]const char, flags1: UINT, flags2: UINT, code: **ID3DBlob, errors: **ID3DBlob) -> HRESULT = {}
cinclude passes an #include through to the C file. [external] marks a name the headers already define: rin type-checks every use of it but emits no definition, and the C compiler sees the header's own declaration. A struct declared struct[external] lists only the fields our lessons touch, and its layout always comes from the header. So the file is a checked list of exactly the Windows API we use, a little over six hundred lines, and nothing in it is a copy of anything the SDK defines.
Finding resources
The programs read files while they run: their HLSL source, and the textures and models in resources/. LearnOpenGL handles this with a FileSystem::getPath helper whose root is configured when the project is built. Ours, in learn/filesystem.rin, finds the root when the program starts instead: it takes the executable's own path and walks up one folder at a time until it reaches a folder that contains resources.
filesystem_path: proc(relative: [*]const char) -> [*]const char = {
if (filesystem_root[0] == 0) { filesystem_find_root(); }
A lesson then names every file relative to the top of the repository:
path: [*]const char = filesystem_path("src/1.getting_started/2.1.hello_triangle/hello_triangle.hlsl");
The executable is in build\cmake\, two folders below the root, so the search succeeds after two steps. It does not matter which directory you start the program from, so double-clicking it in Explorer works as well as running it from a terminal. If the executable is moved somewhere with no resources folder above it, the program says so and exits.
Since shaders are read and compiled every time a lesson starts, editing a .hlsl file needs no rebuild at all: save it and run the program again. Only changes to .rin files need rin build.
That is the whole setup. With the tools on the PATH and one rin build behind us, we are ready to look at what the first program actually does to get a window on screen.
Additional resources
- rin: the compiler, its standard library and its documentation.
- cmake(1): CMake's command line, including the configure and
--buildstepsrin buildruns. - The Ninja build system: what Ninja does and why it is fast.
- MSVC compatibility: how clang-cl fits into the Windows toolchain.
- Software layers: the Direct3D debug layer, worth turning on while developing.
- LearnOpenGL's Creating a window: the GLFW and GLAD set-up this chapter replaces.
Exercises
- Run
hello_window.exefrom a different working directory, for examplecd buildand thencmake\hello_window.exe, and thenhello_triangle.exethe same way. Both still find their files. Now copyhello_triangle.exeto a folder outside the repository and run it there: what happens, and which line offilesystem.rinprints the message? - Add a lesson of your own: copy the
1.1.hello_windowfolder to1.2.my_window, rename the file inside tomy_window.rin, add it tobuild_entriesinbuild.rinand runrin build. (The array's length is part of its type, so it has to go from 38 to 39.) - Put a typing mistake in
hello_triangle.hlsland runhello_triangle.exewithout rebuilding. The program prints the compiler's message, with the file and line, and exits. Then put a mistake inhello_triangle.rinand runrin build, and compare where that error is reported.