Hello Window
Time to get something on screen. Our first program opens a window, connects Direct3D to it, and fills it with one colour, over and over, until we close it. That sounds like very little, and in pixels it is, but every later program starts with exactly this code, so it is worth understanding each line. The whole program is hello_window.asm, under two hundred lines with its comments. This lesson and the next write all of it out; from Shaders on, the same code lives in learn/window.asm, and a lesson starts with one call to
It begins by including what it needs: the MASM64 SDK's Windows declarations and the Direct3D ones, the project's macros, and the string and print routines for error messages.
include rt.inc
include d3d11.inc
include macros.asm
include cstr.asm
include print.asm
; ...
.data
; ...
; settings
scr_width dword 1280
scr_height dword 720
The two settings are the size of the area we draw into, in pixels: two 32-bit globals in the .data section. Then
- register a window class and create a window,
- create the Direct3D device, its immediate context and a swap chain for the window,
- create a render target view of the swap chain's back buffer,
- loop: handle the window's messages and input, clear, present.
The window
On Windows, a window doesn't call us to report what happens to it; it sends
align 16
window_proc proc hwnd:qword, msg:qword, wparam:qword, lparam:qword
cmp dword ptr msg, WM_DESTROY
jne default
invoke PostQuitMessage, 0
xor eax, eax
ret
default:
invoke DefWindowProcA, hwnd, msg, wparam, lparam
ret
window_proc endp
Every message gets the window it is for, a number saying which message it is, and two parameters whose meaning depends on the message. We react to exactly one, WM_DESTROY, which arrives when the window is being destroyed: we ask for the program to end, which we come back to in the render loop, and return 0 in rax (xor eax, eax clears all of rax, since writing a 32-bit register zeroes the top half). Everything else goes to rax, so we return it as it is.
proc with named arguments is MASM's: hwnd, msg, wparam and lparam arrive in rcx, rdx, r8 and r9, and the MASM64 SDK's prologue stores them in their shadow-space slots on the stack, where the names refer to them. So msg is a memory operand, and the names keep their values across the calls the procedure makes, whatever those calls do to the registers. x64 Windows has a single calling convention, so a procedure that Windows calls back needs nothing special. The message number is 32 bits wide in a 64-bit slot, hence dword ptr in the comparison.
Before a window can be made, Windows wants a
call SetProcessDPIAware
invoke mem_zero, ADDR wc, sizeof WNDCLASSEX
mov wc.cbSize, sizeof WNDCLASSEX
lea rax, window_proc
mov wc.lpfnWndProc, rax
invoke GetModuleHandleA, 0
mov wc.hInstance, rax
invoke LoadCursorA, 0, IDC_ARROW
mov wc.hCursor, rax
lea rax, window_class_name
mov wc.lpszClassName, rax
invoke RegisterClassExA, ADDR wc
The class records the window procedure, the module it lives in (our executable, from window_class_name byte "LearnD3D11", 0 in the data section. The EX version of the structure starts with its own size, cbSize, which Windows checks. Before any of that,
wc is a local of local wc:WNDCLASSEX: a structure-sized space in the procedure's stack frame. It starts out holding whatever was on the stack, so we zero it with cstr.asm) and then set only the fields we need. Win32 and Direct3D structures have many fields whose zero is the sensible default, so the lessons write every such structure this way: zero it, then one mov per field. An address can't be moved straight into memory, so lea rax, window_proc computes it into a register first. And ADDR wc in an &wc.
Now the window itself:
mov style, WS_OVERLAPPEDWINDOW and not (WS_THICKFRAME or WS_MAXIMIZEBOX)
mov rect.left, 0
mov rect.top, 0
mov eax, scr_width
mov rect.right, eax
mov eax, scr_height
mov rect.bottom, eax
invoke AdjustWindowRect, ADDR rect, style, 0
or style, WS_VISIBLE
mov eax, rect.right
sub eax, rect.left
mov r10d, rect.bottom
sub r10d, rect.top
invoke CreateWindowExA, 0, ADDR window_class_name, "LearnD3D11", style, CW_USEDEFAULT, CW_USEDEFAULT, rax, r10, 0, 0, wc.hInstance, 0
test rax, rax
jnz window_created
print "Failed to create the window\n"
invoke ExitProcess, 1
window_created:
mov window, rax
WS_OVERLAPPEDWINDOW is the usual top-level window, with a title bar, a border and the minimise, maximise and close buttons. We remove two parts of it: the thick frame that lets the user resize the window, and the maximise button. Our window has a fixed size, for a reason we will see shortly.
The size we pass to
A few things are assembly's own. The style is worked out by the assembler, not at run time: and, not and or inside an operand are operators on constants, so the mov stores a single number. The width and height are computed into rax and r10 before the call, because
The device and the swap chain
LearnOpenGL at this point creates an OpenGL context, makes it current, and asks GLAD to load the OpenGL functions. Our counterpart is a single call that creates all three objects from the Direct3D 11 chapter: the device, the immediate context and the swap chain. The swap chain needs a description first:
invoke mem_zero, ADDR swap_desc, sizeof DXGI_SWAP_CHAIN_DESC
mov eax, scr_width
mov swap_desc.BufferDesc._Width, eax
mov eax, scr_height
mov swap_desc.BufferDesc.Height, eax
mov swap_desc.BufferDesc.Format, DXGI_FORMAT_R8G8B8A8_UNORM
mov swap_desc.SampleDesc.Count, 1
mov swap_desc.BufferUsage, DXGI_USAGE_RENDER_TARGET_OUTPUT
mov swap_desc.BufferCount, 2
mov rax, window
mov swap_desc.OutputWindow, rax
mov swap_desc.Windowed, 1
mov swap_desc.SwapEffect, DXGI_SWAP_EFFECT_FLIP_DISCARD
A field that is itself a structure is reached with a second dot, as in swap_desc.BufferDesc.Height. _Width has an underscore because width is a word MASM reserves; d3d11.inc renames every such field the same way. Copying from one place in memory to another always goes through a register, here eax or rax, since an x64 mov can't take two memory operands.
A swap chain is a small set of images, its
- The same size as the client area, with four 8-bit channels per pixel: red, green, blue and alpha. UNORM means each channel's integer 0..255 is read as a number from 0.0 to 1.0.
- One sample per pixel, so no multisampling. Multisampled rendering is done differently in Direct3D 11 and gets its own chapter, Anti Aliasing.
- Used as render target output, which means we will draw into them.
- Two buffers, shown in our window, in a window rather than full screen.
- The
flip model of presentation, DXGI_SWAP_EFFECT_FLIP_DISCARD. Windows' compositor shows our finished buffer directly instead of copying it, and "discard" means we make no assumptions about what a buffer contains after it has been shown. We clear it every frame anyway.
With that, we create everything:
mov feature_level, D3D_FEATURE_LEVEL_11_0
invoke D3D11CreateDeviceAndSwapChain, 0, D3D_DRIVER_TYPE_HARDWARE, 0, 0, ADDR feature_level, 1, D3D11_SDK_VERSION, \
ADDR swap_desc, ADDR swap_chain, ADDR device, 0, ADDR context
test eax, eax
jns device_created
print "Failed to create the D3D11 device\n"
invoke ExitProcess, 1
device_created:
Taking the arguments in order: 0 for the adapter means the system's default GPU; D3D_DRIVER_TYPE_HARDWARE asks for a real GPU rather than a software rasterizer; the next 0 is the software module, which only applies to software drivers; the one after it means no creation flags. Then comes the list of feature levels we accept, which is just 11_0, and its length, and D3D11_SDK_VERSION, which lets the runtime check that it matches the headers we compiled against. The rest are our swap chain description and the addresses where the swap chain, the device, the feature level obtained (we pass 0, since we accept only one) and the immediate context are written. The swap chain, device and context are three qword globals, each holding a pointer once the call returns. The backslash at the end of the first line continues the
Like most Direct3D functions, it returns an HRESULT in eax, negative on failure, and this is one call where failure is worth checking: on a machine without a feature level 11_0 GPU it is the first thing to go wrong. test eax, eax sets the sign flag from the top bit, and jns jumps past the error when it is clear.
The render target view
We have a swap chain, but we cannot draw into it yet. The pipeline draws into resources only through views (see the Direct3D 11 chapter), so we fetch the swap chain's back buffer as a texture and create a
vcall swap_chain, IDXGISwapChain_GetBuffer, 0, ADDR IID_ID3D11Texture2D, ADDR backbuffer
vcall device, ID3D11Device_CreateRenderTargetView, backbuffer, 0, ADDR backbuffer_rtv
These are our first COM calls, made with this. d3d11.inc, and it writes the pointer into backbuffer. C needs a cast on each call, to an untyped pointer and to the base interface ID3D11Resource; to the assembler a pointer is a qword, and there is nothing to cast. The 0 is an optional view description; without one, the view covers the whole texture in the texture's own format.
One view is enough for the life of the program, even though there are two buffers taking turns. In Direct3D 11, buffer 0 of a flip-model swap chain always refers to whichever buffer is currently the back buffer, and the runtime moves the view along with it after each present.
Viewport
OpenGL has to be told the size of the area it renders into, with hello_window.asm doesn't set one yet. The next program fills one in like this, and from Shaders on
invoke mem_zero, ADDR viewport, sizeof D3D11_VIEWPORT
cvtsi2ss xmm0, scr_width
movss viewport._Width, xmm0
cvtsi2ss xmm0, scr_height
movss viewport.Height, xmm0
storef viewport.MaxDepth, 1.0
The viewport's fields are floats, so these are our first floating-point instructions. Floats live in the SSE registers, xmm0 to xmm15: cvtsi2ss converts the integer width into a single float in xmm0, and movss stores it. A constant like 1.0 needs one more step, because MASM only turns 1.0 into bits when it is data: macros.asm, puts the constant in the read-only data section and copies its four bytes into the field.
The viewport is the rectangle of the render target that the final, normalized coordinates are mapped onto: here, the whole window, starting at its top-left corner, with depth values from 0 to 1. A point that comes out of the pipeline at \((x, y)\), with both running from -1 to 1, lands on pixel
\[ x_{\text{pixel}} = \frac{x + 1}{2} \cdot \text{Width}, \qquad y_{\text{pixel}} = \frac{1 - y}{2} \cdot \text{Height} \]The \(1 - y\) is the one difference from OpenGL here. Direct3D counts pixel rows from the top of the window, OpenGL from the bottom, so in Direct3D a larger \(y\) means a smaller row number. Up is still up on screen: the point \((-0.5, 0.5)\) is in the upper left quarter in both APIs, and in our 1280 by 720 window it is the pixel \((320, 180)\), counted from the top-left corner.
LearnOpenGL also registers a callback to update the viewport whenever the window is resized. We have none, because our window cannot be resized. Resizing in Direct3D means more than a new viewport: the swap chain's buffers have to be resized, and every view of them, and every render target the size of the window (later chapters have several), has to be recreated. A fixed size keeps all of that out of the lessons.
The render loop
We don't want to draw one picture and quit. The program runs a loop, the
mov running, 1
render_loop:
pump:
invoke PeekMessageA, ADDR msg, 0, 0, 0, PM_REMOVE
test eax, eax
jz pumped
cmp dword ptr msg.message, WM_QUIT
jne not_quit
mov running, 0
not_quit:
invoke TranslateMessage, ADDR msg
invoke DispatchMessageA, ADDR msg
jmp pump
pumped:
cmp running, 0
je render_loop_done
; input
; -----
call process_input
; render
; ------
vcall context, ID3D11DeviceContext_ClearRenderTargetView, backbuffer_rtv, floats(0.2, 0.3, 0.3, 1.0)
; dxgi: show the image just drawn, waiting for the next vertical blank
; --------------------------------------------------------------------
vcall swap_chain, IDXGISwapChain_Present, 1, 0
jmp render_loop
render_loop_done:
invoke ExitProcess, 0
Assembly has no while: a loop is a label, a test, and a jump back. The indentation only shows the structure to the reader; the labels and jumps are the structure. The outer loop runs from render_loop to the jmp render_loop at the bottom, and the inner one, from pump to jmp pump, runs until jz pumped leaves it. Labels inside a procedure are local to it, so every lesson can have its own render_loop.
It has three parts. First the
Then input and rendering, which the next sections fill in. And last,
Double buffering
If we drew straight into the image the monitor was showing, it would show our work in progress: half-cleared frames, objects drawn but not yet covered by the ones in front, and flicker. The swap chain avoids this with two buffers. The
The first argument to glfwSwapInterval(1). The second argument holds flags, none here.
One last thing
The loop runs while running is set, and it is cleared by one message: WM_QUIT. Here is how that message comes about. Clicking the close button sends the window WM_CLOSE; our procedure passes it to invoke ExitProcess, 0. With /entry:main there is no C runtime that called
LearnOpenGL ends with
Build it (scripts\build.bat, as in the previous chapter) and run build\hello_window.exe, and you should see this:
A plain teal rectangle: if that's what you got, everything is working. The colour comes from the clear we skipped over, which is the last part of this chapter, just after input.
Input
The Escape key should close the window. LearnOpenGL asks GLFW with test ax, ax sets the sign flag from that bit, and jns skips ahead when it is clear.
; process all input: ask Windows whether relevant keys are down this frame and react accordingly
; ----------------------------------------------------------------------------------------------
; The top bit of GetAsyncKeyState is set while a key is held, which makes the SHORT negative.
align 16
process_input proc
invoke GetAsyncKeyState, VK_ESCAPE
test ax, ax
jns done
invoke DestroyWindow, window
done:
ret
process_input endp
When Escape is down, we call
There is one catch. learn/window.asm, which first checks that our window is the one in front, which is what
; rcx: virtual key -> rax: 1 while it is held. The top bit of GetAsyncKeyState is set while a key
; is down, which makes the SHORT negative.
align 16
key_down proc vkey:qword
invoke GetForegroundWindow
cmp rax, window
jne up
invoke GetAsyncKeyState, vkey
test ax, ax
jns up
mov eax, 1
ret
up:
xor eax, eax
ret
key_down endp
Polling like this suits keys held down, like movement keys later on. Keys that should do something once per press, like switching modes with Tab, come in as WM_KEYDOWN messages to the window procedure instead; we will see that in the next lesson.
Rendering
Each frame we want to start from a clean picture; otherwise we would see whatever was in the buffer before (with the flip model's discard, that could be anything). So the render part of the loop clears the back buffer to a colour of our choice:
vcall context, ID3D11DeviceContext_ClearRenderTargetView, backbuffer_rtv, floats(0.2, 0.3, 0.3, 1.0)
The colour is four floats, red, green and blue (0.2, 0.3, 0.3 is a dark blue-green) and alpha, which is 1, fully opaque. The method wants their address, and macros.asm, provides it: it places the four values in the read-only data section, sixteen bytes aligned, and stands for ADDR of them. Compare this with OpenGL, which sets the clear colour as a piece of state with
Everything we draw in later chapters goes between this clear and the
Additional resources
- Your first Windows program: Microsoft's introduction to window classes, window procedures and the message loop.
- DXGI flip model: how flip-model swap chains present, and why they are preferred.
- D3D11CreateDeviceAndSwapChain: the reference for every parameter used above.
- High DPI desktop application development: what
SetProcessDPIAware opts into. - LearnOpenGL's Hello Window: the same program with GLFW.
Exercises
- Make the clear colour change over time.
learn/window.asmhas aget_time procedure built onQueryPerformanceFrequency andQueryPerformanceCounter , which the shaders lesson uses; write its like intohello_window.asm, includemath.asmforsinf (angle inxmm0, result inxmm0), and drive one of the colour channels with it.floats makes a constant, so the colour needs a writable home in.datainstead, such asclear_color real4 0.2, 0.3, 0.3, 1.0, passed asADDR clear_color. - Change the sync interval passed to
Present from 1 to 0. Count frames, and once a second show the count in the title bar withSetWindowTextA , after formatting it into a buffer withwsprintfA . Compare it with the count at interval 1. - Make the space bar close the window too. Then make it do something that should happen once per press, such as toggling between two clear colours, and see why polling
GetAsyncKeyState every frame is the wrong tool for that.