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:
ml64 , the Microsoft Macro Assembler for x64 (MASM), turns a.asmfile into an object file. Besides instructions it understands structures, procedures with named arguments and locals, and a macro language, which is what makes code at this length readable.link , Microsoft's linker, turns that object file into an.exe, resolving each Windows function it calls against an import library, and writes the debug information to a.pdbfile beside it.- Both come with
Visual Studio (2022 or later), or the free Build Tools for Visual Studio, with the "Desktop development with C++" workload, which holds the x64 tools. The same installation brings theWindows SDK : the import libraries for everything we call,kernel32.lib,user32.lib,ole32.lib,d3d11.libandd3dcompiler.lib. - The
MASM64 SDK , from masm32.com, provides what ml64 lacks out of the box: the Windows structures and constants (win64.inc), the declarations of the Win32 functions, and the macros we lean on most,invoke for calling a function with its arguments, and theSTACKFRAMEprologue that gives each procedure its stack frame. It is not part of the repository. Install it intoC:\masm64, or set the environment variableMASM64_HOMEto wherever it is.
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.
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 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:
kernel32: files, memory, timers, the command line, and ending the process.user32: windows, messages and keyboard state, andwsprintfA , which formats our printed messages.ole32: COM start-up andCoCreateInstance , through which we reach the Windows Imaging Component that decodes our.pngand.jpgfiles.d3d11: Direct3D 11, includingD3D11CreateDeviceAndSwapChain , which also creates the DXGI swap chain.d3dcompiler:D3DCompile , which compiles our HLSL shaders while the program starts.
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 /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 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 saha.asm is an arena that stands in for 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
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 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.
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
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
- The MASM32 SDK and MASM64 SDK: the include files and macros, and a forum full of people who write this kind of code.
- Microsoft Macro Assembler reference: ml64's directives, operators and command line.
- x64 calling convention: registers, shadow space and stack alignment, which every call in these programs follows.
- Linker options:
/entry,/subsystem,/debugand the rest. - 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_triangle.exefrom a different working directory, for examplecd buildand thenhello_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 offilesystem.asmprints the message? - Add a lesson of your own: copy the
1.1.hello_windowfolder to1.2.my_window, rename the file inside tomy_window.asm, and runscripts\build.bat my_window. Nothing needs registering anywhere: the script builds whatever.asmfiles it finds undersrc/. - 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.asm, say a misspelt register, and runscripts\build.bat hello_triangle, and compare where that error is reported. Finally, deleted3dcompiler.libfrom the libs line inbuild.batand build again: which step fails now, and how does it say so?