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:

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.

Nothing else needs installing. LearnOpenGL's projects depend on GLFW, GLAD, GLM and stb_image. Our equivalents are all either part of Windows (Win32 windows, Direct3D, the shader compiler in 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:

The single define, _CRT_SECURE_NO_WARNINGS, silences the C runtime's warnings about functions like fopen, which the helpers use to read files.

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

Exercises

  1. Run hello_window.exe from a different working directory, for example cd build and then cmake\hello_window.exe, and then hello_triangle.exe the same way. Both still find their files. Now copy hello_triangle.exe to a folder outside the repository and run it there: what happens, and which line of filesystem.rin prints the message?
  2. Add a lesson of your own: copy the 1.1.hello_window folder to 1.2.my_window, rename the file inside to my_window.rin, add it to build_entries in build.rin and run rin build. (The array's length is part of its type, so it has to go from 38 to 39.)
  3. Put a typing mistake in hello_triangle.hlsl and run hello_triangle.exe without rebuilding. The program prints the compiler's message, with the file and line, and exits. Then put a mistake in hello_triangle.rin and run rin build, and compare where that error is reported.
Credit: this chapter follows LearnOpenGL's Creating a window by Joey de Vries, rewritten for Direct3D 11 and rin. LearnOpenGL's text and code are licensed CC BY-NC 4.0, and so is this page; the wording, code and screenshots here are new, and diagrams marked as LearnOpenGL's are its own.