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 window_create.

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 main does four things in order, which is a good map for the rest of this chapter:

  1. register a window class and create a window,
  2. create the Direct3D device, its immediate context and a swap chain for the window,
  3. create a render target view of the swap chain's back buffer,
  4. 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 messages. A key press, a mouse move, a click on the close button: each arrives as a message, and Windows delivers it to a function we provide, the window procedure. GLFW has one of these hidden inside it and turns the messages into its callbacks. Ours is the smallest a window procedure can be:

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 DefWindowProcA, Windows' default handling, which is what makes the window move when dragged, redraw its title bar, and destroy itself when the close button is clicked; its result is already in 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 window class: a named template saying which procedure handles the messages for windows of that kind. We fill in a WNDCLASSEX and register it:

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 GetModuleHandleA), the ordinary arrow cursor, and a name, 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, SetProcessDPIAware tells Windows that we work in real pixels. Without it, on a display scaled to 150%, Windows would treat our sizes as "logical" pixels: the window would cover 1920 by 1080 real pixels, and our 1280 by 720 picture would be stretched to fill it, blurring everything we draw.

wc is a local of main, declared as 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 mem_zero (from 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 invoke passes the structure's address, where C would write &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 CreateWindowExA is the size of the whole window, borders and title bar included. We want the inside, the client area where we draw, to be exactly 1280 by 720, so we put that rectangle into AdjustWindowRect, which grows it by the size of the frame for the given style. The other arguments are the class name, the title shown in the title bar, the style plus WS_VISIBLE so the window appears at once, and CW_USEDEFAULT to let Windows pick its position. What comes back is an HWND, a handle to the window, which we keep in the global window. We need it for the swap chain and for input.

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 CreateWindowExA has twelve arguments, and from the fifth on they go on the stack: invoke stores a register there as it is, but the four argument registers are filled first, so a value we compute must not be left in one of them. A failed call returns 0; we print a message with print, this project's stand-in for printf built on wsprintfA, and end the process with ExitProcess, since there is no C runtime to return to.

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 buffers, which take turns being drawn into and being shown in the window. The description says what they are like:

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 invoke on the next.

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 context version and profile hints LearnOpenGL gives GLFW are our feature-level list: they say what we need, and creation fails if the hardware can't provide it. There is no "make current" step. Direct3D calls go to the device or context we name in each call, not to an implicit current one.

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 render target view of it:

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 vcall as the Direct3D 11 chapter described: the object, the method, then the arguments that follow the hidden this. GetBuffer can return a buffer as several kinds of interface, so we pass the address of the GUID of the one we want, IID_ID3D11Texture2D, sixteen bytes of data from 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 glViewport. Direct3D needs the same information, as a D3D11_VIEWPORT bound with RSSetViewports, before anything is drawn. This first program draws nothing, only clears, and clearing does not use the viewport, so hello_window.asm doesn't set one yet. The next program fills one in like this, and from Shaders on window_create does the same for every lesson:

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: storef, a macro from 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 render loop, and each turn of it, a frame, handles what happened since the last frame and draws a new picture. Here is ours, in full:

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 message loop, our glfwPollEvents. Messages for our window wait in a queue, and PeekMessageA takes the next one out, returning zero when the queue is empty, so the inner loop empties the queue and moves on. (Its relative GetMessageA would wait for a message to arrive, which suits a text editor but would freeze a game.) TranslateMessage turns key presses into character messages, which we don't use but which is customary, and DispatchMessageA hands the message to our window procedure.

Then input and rendering, which the next sections fill in. And last, Present, the counterpart of glfwSwapBuffers.

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 front buffer is shown while we draw into the back buffer, and Present swaps their roles when we are done, so the screen only ever shows finished frames.

front bufferon screen back bufferwe clear and draw here Present and the old front becomes the next back

The first argument to Present is the sync interval. With 1, the new frame is shown at the monitor's next vertical blank, the moment between two screen refreshes, and if we are early the call waits for it. So our loop runs at the display's refresh rate, typically 60 times a second, and doesn't keep a CPU core busy drawing frames nobody sees. That is vertical sync; GLFW does the same with 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 DefWindowProcA, which destroys the window; destroying it sends WM_DESTROY, where we call PostQuitMessage; and that puts WM_QUIT into the queue, where the message loop finds it. The loop ends, and main ends the process with invoke ExitProcess, 0. With /entry:main there is no C runtime that called main, so there is nobody to return to.

LearnOpenGL ends with glfwTerminate to clean up. We don't release our Direct3D objects one by one at the end: when a process exits, Windows frees everything it owned, GPU resources included. Releasing matters for objects a program throws away while it keeps running, and we will do it where that happens.

Build it (scripts\build.bat, as in the previous chapter) and run build\hello_window.exe, and you should see this:

An empty window cleared to dark teal

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 glfwGetKey whether a key is down at the moment; the Win32 counterpart is GetAsyncKeyState. It returns a 16-bit value whose top bit is set while the key is held, and a 16-bit value with its top bit set is negative, which makes the test short: 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 DestroyWindow, which takes the same path as the close button from WM_DESTROY on. process_input is called once per frame, between the message loop and rendering, so the key is checked about sixty times a second.

There is one catch. GetAsyncKeyState reads the state of the physical keyboard, whichever window has the focus, so as written, pressing Escape in some other program closes ours too. This first lesson keeps it that simple. From Shaders on, keys are read through key_down in learn/window.asm, which first checks that our window is the one in front, which is what glfwGetKey gives LearnOpenGL:

; 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 floats, another macro from 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 glClearColor and then clears with glClear, which reads that state. In Direct3D the colour is simply an argument, and the call names the view it clears. Clearing does not depend on which render targets are bound, or on the viewport: it always fills the whole view.

Everything we draw in later chapters goes between this clear and the Present at the bottom of the loop. The full program is here. In the next chapter, we draw into the window for real.

Additional resources

Exercises

  1. Make the clear colour change over time. learn/window.asm has a get_time procedure built on QueryPerformanceFrequency and QueryPerformanceCounter, which the shaders lesson uses; write its like into hello_window.asm, include math.asm for sinf (angle in xmm0, result in xmm0), and drive one of the colour channels with it. floats makes a constant, so the colour needs a writable home in .data instead, such as clear_color real4 0.2, 0.3, 0.3, 1.0, passed as ADDR clear_color.
  2. Change the sync interval passed to Present from 1 to 0. Count frames, and once a second show the count in the title bar with SetWindowTextA, after formatting it into a buffer with wsprintfA. Compare it with the count at interval 1.
  3. 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.
Credit: this chapter follows LearnOpenGL's Hello 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.