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.rin, about 120 lines.

It begins by importing what it needs: the Windows and Direct3D declarations, and rin's standard print functions for error messages.

import "../../../includes/win32.rin"
import "std/print.rin"

// settings
scr_width: i32 = 1280;
scr_height: i32 = 720;

The two settings are the size of the area we draw into, in pixels. Then the program 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:

window_proc: proc[callconv(CALLBACK)](hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM) -> LRESULT = {
    if (msg == WM_DESTROY) {
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProcA(hwnd, msg, wparam, lparam);
}

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. 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. callconv(CALLBACK) gives the procedure the calling convention Windows uses for callbacks; Windows calls this function, so it must agree with Windows on how arguments are passed.

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 WNDCLASSA and register it:

SetProcessDPIAware();
instance: HMODULE = GetModuleHandleA(null);
wc: WNDCLASSA = {
    .lpfnWndProc = window_proc,
    .hInstance = instance,
    .hCursor = LoadCursorA(null, IDC_ARROW),
    .lpszClassName = "LearnD3D11",
};
RegisterClassA(wc&);

The class records the window procedure, the module it lives in (our executable, from GetModuleHandleA), the ordinary arrow cursor, and a name. 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.

The braces are a designated initializer: each .field = value sets one field by name, and every field we don't name starts at zero. Win32 and Direct3D structs have many fields whose zero is the sensible default, so naming only the ones we need keeps each description short, and the lessons write every such struct this way. A field that is itself a struct gets braces of its own, as the swap chain's BufferDesc does below.

Now the window itself:

style: DWORD = WS_OVERLAPPEDWINDOW & ~(WS_THICKFRAME | WS_MAXIMIZEBOX);
rect: RECT = { .right = scr_width, .bottom = scr_height };
AdjustWindowRect(rect&, style, 0);
window: HWND = CreateWindowExA(0, wc.lpszClassName, "LearnD3D11", style | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT,
                               rect.right - rect.left, rect.bottom - rect.top, null, null, instance, null);
if (window == null) {
    printf("Failed to create the window\n");
    return -1;
}

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. We need it for the swap chain and for input.

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:

swap_desc: DXGI_SWAP_CHAIN_DESC = {
    .BufferDesc = {
        .Width = cast(scr_width, UINT),
        .Height = cast(scr_height, UINT),
        .Format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM,
    },
    .SampleDesc = { .Count = 1 },
    .BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT,
    .BufferCount = 2,
    .OutputWindow = window,
    .Windowed = 1,
    .SwapEffect = DXGI_SWAP_EFFECT.DXGI_SWAP_EFFECT_FLIP_DISCARD,
};

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:

feature_level: D3D_FEATURE_LEVEL = D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_0;
swap_chain: *IDXGISwapChain = null;
device: *ID3D11Device = null;
context: *ID3D11DeviceContext = null;
if (D3D11CreateDeviceAndSwapChain(null, D3D_DRIVER_TYPE.D3D_DRIVER_TYPE_HARDWARE, null, 0, feature_level&, 1, D3D11_SDK_VERSION,
                                  swap_desc&, swap_chain&, device&, null, context&) < 0) {
    printf("Failed to create the D3D11 device\n");
    return -1;
}

Taking the arguments in order: null 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 null is the software module, which only applies to software drivers; 0 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 null, since we accept only one) and the immediate context are written.

Like most Direct3D functions, it returns an HRESULT, 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.

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:

backbuffer: *ID3D11Texture2D = null;
IDXGISwapChain_GetBuffer(swap_chain, 0, IID_ID3D11Texture2D&, cast(backbuffer&, **void));
backbuffer_rtv: *ID3D11RenderTargetView = null;
ID3D11Device_CreateRenderTargetView(device, cast(backbuffer, *ID3D11Resource), null, backbuffer_rtv&);

GetBuffer can return a buffer as several kinds of interface, so we pass the GUID of the one we want, IID_ID3D11Texture2D, and it writes an untyped pointer, hence the cast. CreateRenderTargetView takes any resource, so the texture is cast to the base interface ID3D11Resource. The null 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.rin doesn't set one yet. From the next program on, every lesson does it in the same way:

viewport: D3D11_VIEWPORT = {
    .Width = cast(scr_width, f32),
    .Height = cast(scr_height, f32),
    .MaxDepth = 1.0,
};

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:

running: b32 = 1;
while (running) {
    msg: MSG = {};
    while (PeekMessageA(msg&, null, 0, 0, PM_REMOVE) != 0) {
        if (msg.message == WM_QUIT) { running = 0; }
        TranslateMessage(msg&);
        DispatchMessageA(msg&);
    }

    // input
    // -----
    process_input(window);

    // render
    // ------
    clear_color: [4]f32 = { 0.2, 0.3, 0.3, 1.0 };
    ID3D11DeviceContext_ClearRenderTargetView(context, backbuffer_rtv, clear_color[0]&);

    // dxgi: show the image just drawn, waiting for the next vertical blank
    // --------------------------------------------------------------------
    IDXGISwapChain_Present(swap_chain, 1, 0);
}

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, main returns 0, and the program exits.

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 (rin build, as in the previous chapter) and run build\cmake\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 SHORT with its top bit set is negative, which makes the test short:

key_down: proc(key: i32) -> b32 = {
    return GetAsyncKeyState(key) < 0;
}

process_input: proc(window: HWND) -> void = {
    // GetAsyncKeyState reads the whole keyboard, so ignore it while another
    // window has the focus; glfwGetKey only ever sees its own window's keys.
    if (GetForegroundWindow() != window) { return; }
    if (key_down(VK_ESCAPE)) { DestroyWindow(window); }
}

There is one catch, which the comment points out. GetAsyncKeyState reads the state of the physical keyboard, whichever window has the focus. Without the GetForegroundWindow check, pressing Escape in some other program would close ours. With it, we only react while our window is in front, which is what glfwGetKey gives LearnOpenGL.

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. 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 later lessons.

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:

clear_color: [4]f32 = { 0.2, 0.3, 0.3, 1.0 };
ID3D11DeviceContext_ClearRenderTargetView(context, backbuffer_rtv, clear_color[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. 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. The shaders lesson has a get_time procedure built on QueryPerformanceCounter; copy it, import math.rin for sinf, and drive one of the colour channels with it.
  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. 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 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.