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:
- 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
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 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
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
.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
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
- 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:
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 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
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&);
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 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
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
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
LearnOpenGL ends with
Build it (rin build, as in the previous chapter) and run build\cmake\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 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.
When Escape is down, we call
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
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. The shaders lesson has a
get_time procedure built onQueryPerformanceCounter ; copy it, importmath.rinforsinf , and drive one of the colour channels with it. - 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 . 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.