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. The first two lessons write that code out themselves, and the next chapter goes through it line by line; from the third lesson on it lives in one include, learn/window.asm, which plays GLFW's part.

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 x64 assembly for Microsoft's assembler. There is no compiler in the chain and no C runtime: each lesson is assembled into one object file and linked into an executable, and that is the whole build. Each tool does one job:

Nothing needs to be on the PATH. The build script looks for Visual Studio itself, with vswhere, and runs its vcvars64.bat to put ml64 and link within reach; from an "x64 Native Tools Command Prompt" it uses the tools that are already there.

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 assembly 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

scripts\build.bat
build\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. Given an argument, the script builds only the lessons whose file names contain it: scripts\build.bat hello builds hello_window and hello_triangle.

build.bat

There is no project file and no build system. scripts\build.bat is a short batch file, and the heart of it is three settings and a loop. Here they are, with the checks around them cut:

if not defined MASM64_HOME set MASM64_HOME=C:\masm64
...
set mlargs=/nologo /c /Cp /Zi /Zd /W2 /I"%root%\includes" /I"%MASM64_HOME%\include64" /I"%MASM64_HOME%\macros64"
set libs=kernel32.lib user32.lib ole32.lib d3d11.lib d3dcompiler.lib
set linkargs=/nologo /debug:full /entry:main /subsystem:console /machine:x64 /opt:ref /opt:noicf %libs%
...
for /r "%root%\src" %%f in (*.asm) do (
    ...
        ml64 %mlargs% /Fo"%root%\build\obj\%%~nf.obj" "%%f" > "%root%\build\obj\%%~nf.log"
        ...
            link "%root%\build\obj\%%~nf.obj" %linkargs% /out:"%root%\build\%%~nf.exe" /pdb:"%root%\build\%%~nf.pdb" > "%root%\build\obj\%%~nf.link.log"
    ...
)

The loop visits every .asm file under src/, thirty-eight of them, one per lesson, and makes each a separate executable named after its file. For each one it runs ml64 once and link once. The assembler's options say: only assemble (/c, linking is a separate step), keep the case of names as written (/Cp, since Windows' names are case-sensitive), write debug information (/Zi, /Zd), and search three folders for includes: the repository's includes/, and the MASM64 SDK's include64 and macros64. The executables land in build\, the object files, and whatever the assembler and linker printed, in build\obj\.

One file, one program

Lessons share no code at link time. Each .asm is a unity build: it includes what it uses, the includes include what they need, and the assembler reads the lot as one long source file. That is why there is one object file per lesson and nothing to link it with but the system's import libraries. Every include starts with a guard, so including one twice is harmless:

ifndef window_asm
window_asm = 0

The debug information refers to the original files and lines, includes and all. Open hello_window.exe in the Visual Studio debugger, or WinDbg, or any debugger that reads .pdb files, and breakpoints and single-stepping work on the .asm source, with the registers in view.

Linking

The libs line 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:

A C program would also link dxguid.lib for the interface GUIDs, such as IID_ID3D11Texture2D. We don't: our GUIDs are plain data, sixteen bytes each, in d3d11.inc. The linker options say the rest. /entry:main makes our main procedure the very first code that runs, with no C runtime start-up before it; a lesson ends by calling ExitProcess, since there is nothing to return to. /subsystem:console gives each program a console, where error messages go.

If you leave a library out, the assembler is perfectly happy, 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:

scripts/
  build.bat                   how the lessons are built
src/
  1.getting_started/
    1.1.hello_window/
      hello_window.asm        the program
    2.1.hello_triangle/
      hello_triangle.asm      the program
      hello_triangle.hlsl     its shaders: vs_main, ps_main
    ...
  2.lighting/
  ...
includes/
  rt.inc                      the MASM64 SDK's Windows includes and macros
  d3d11.inc                   D3D11, DXGI, D3DCompiler and WIC: structures, constants, methods, GUIDs
  macros.asm                  vcall, float constants, print, asserts
  cstr.asm, print.asm         strings, memory, and print over wsprintfA
  saha.asm                    the arena allocator
  math.asm                    vectors and matrices, in SSE
  image.asm                   .png and .jpg through WIC, Radiance .hdr
  learn/
    window.asm                the window, the device, input, the clock
    filesystem.asm            paths under the repository root
    shader.asm                compile a .hlsl file, create its input layout
    gpu.asm                   buffers, constants, textures, samplers, render targets
    camera.asm                the fly camera
    model.asm                 Wavefront .obj models and their textures
    ...
resources/                    textures, models, the HDR environment

Every lesson is one .asm file, read top to bottom: a comment saying what the lesson is about, its includes, its data, then main: window, device, shaders, buffers, 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/. d3d11.inc and rt.inc stand in for the GLFW and GLAD headers, math.asm for GLM, and image.asm for stb_image. A few files do what the C library would: cstr.asm has string and memory routines such as mem_zero and mem_copy, and saha.asm is an arena that stands in for malloc. The learn/ folder holds 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 by name, found through the /I folders the build passes:

include rt.inc
include d3d11.inc
include macros.asm
include math.asm
include learn\window.asm
include learn\shader.asm
include learn\gpu.asm

Calling Windows from assembly

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 the assembler needs is to know the names: of the functions, of the constants, and of every structure and its fields, so that swap_desc.BufferDesc.Format means the right byte. Two files provide them. rt.inc pulls in the MASM64 SDK's own includes, for Win32:

include win64.inc
include vasily.inc
include macros64.inc

STACKFRAME

include kernel32.inc
include user32.inc
include gdi32.inc
include ole32.inc

and d3d11.inc covers Direct3D, DXGI, the shader compiler and WIC, which the SDK doesn't. It is generated, by tools/gen_d3d11.py, from the Windows SDK's own C headers: the generator asks the C compiler (through libclang) where every field of every structure sits, and writes each structure with its fields at exactly those offsets, padding included, plus a check of its total size that fails the assembly if it is ever wrong. Constants, the byte offset of every COM method, the GUIDs and the few plain functions we import, such as D3D11CreateDeviceAndSwapChain and D3DCompile, come out of the same pass. Nothing in it is written by hand.

MASM reserves a long list of words, and some are the names of fields in Windows' structures: Width, Size, Flags, Type. The generator renames those with a leading underscore, so a texture description's width is desc._Width. The repository's notes on the assembly list these and the other conventions in one place.

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.asm, finds the root when the program starts instead: it takes the executable's own path from GetModuleFileNameA and walks up one folder at a time until it reaches a folder that contains resources.

; rcx: path relative to the repository root -> rax: the full path
align 16
filesystem_path proc relative:qword
    USING rbx
    SaveRegs
    cmp filesystem_root, 0
    jne have_root
        call filesystem_find_root
    have_root:

A lesson then names every file relative to the top of the repository:

invoke filesystem_path, "src/1.getting_started/2.1.hello_triangle/hello_triangle.hlsl"
mov path, rax

The executable is in build\, one folder below the root, so the search succeeds after one step. 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.

The USING rbx / SaveRegs pair in that procedure is how these lessons keep the Win64 rule that rbx, rsi, rdi and r12-r15 must survive a call: a procedure that uses one saves it on the way in and restores it with RestoreRegs on the way out. A string literal as an argument, as in the invoke above, is something the MASM64 SDK's invoke handles itself, by placing the text in the data section and passing its address.

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 .asm files need scripts\build.bat.

That is the whole setup. With Visual Studio and the MASM64 SDK installed and one scripts\build.bat 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_triangle.exe from a different working directory, for example cd build and then hello_triangle.exe, and from somewhere else entirely with its full path. It still finds its files. Now copy it to a folder outside the repository and run it there: what happens, and which line of filesystem.asm 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.asm, and run scripts\build.bat my_window. Nothing needs registering anywhere: the script builds whatever .asm files it finds under src/.
  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.asm, say a misspelt register, and run scripts\build.bat hello_triangle, and compare where that error is reported. Finally, delete d3dcompiler.lib from the libs line in build.bat and build again: which step fails now, and how does it say so?
Credit: this chapter follows LearnOpenGL's Creating a window by Joey de Vries, rewritten for Direct3D 11 and x64 assembly. 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.