blending.rin

src/4.advanced_d3d11/3.1.blending/blending.rin · 504 lines

// 3.1 Blending
//
// Two kinds of see-through:
//
//   grass    each texel is either fully there or fully not. The pixel shader
//            throws away (clip) pixels whose alpha is low, and what is left is
//            ordinary opaque geometry: order does not matter
//   windows  partly transparent. The blend state mixes each new pixel with what
//            is already in the target: src * a + dst * (1 - a)
//
// Blending reads what is behind, so the windows are drawn after everything
// opaque, from the farthest to the nearest. Drawn in the wrong order, a near
// window would be in the depth buffer first and hide the ones behind it.
//
// The grass and windows are sampled with clamp rather than wrap, so the top row
// of each image is not blended with its bottom row. In D3D a sampler is an
// object of its own, bound to a slot like a texture: this is a matter of binding
// a different one into s0 for those draws.

import "../../../includes/win32.rin"
import "../../../includes/math.rin"
import "../../../includes/image.rin"
import "../../../includes/learn/shader.rin"
import "../../../includes/learn/camera.rin"
import "std/print.rin"

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

// a unit cube: position, normal, texture coordinate; counter-clockwise from outside
cube_vertices: [288]f32 = {
    // right (+x)
     0.5, -0.5,  0.5,    1.0,  0.0,  0.0,   0.0, 1.0,
     0.5, -0.5, -0.5,    1.0,  0.0,  0.0,   1.0, 1.0,
     0.5,  0.5, -0.5,    1.0,  0.0,  0.0,   1.0, 0.0,
     0.5, -0.5,  0.5,    1.0,  0.0,  0.0,   0.0, 1.0,
     0.5,  0.5, -0.5,    1.0,  0.0,  0.0,   1.0, 0.0,
     0.5,  0.5,  0.5,    1.0,  0.0,  0.0,   0.0, 0.0,
    // left (-x)
    -0.5, -0.5, -0.5,   -1.0,  0.0,  0.0,   0.0, 1.0,
    -0.5, -0.5,  0.5,   -1.0,  0.0,  0.0,   1.0, 1.0,
    -0.5,  0.5,  0.5,   -1.0,  0.0,  0.0,   1.0, 0.0,
    -0.5, -0.5, -0.5,   -1.0,  0.0,  0.0,   0.0, 1.0,
    -0.5,  0.5,  0.5,   -1.0,  0.0,  0.0,   1.0, 0.0,
    -0.5,  0.5, -0.5,   -1.0,  0.0,  0.0,   0.0, 0.0,
    // top (+y)
    -0.5,  0.5,  0.5,    0.0,  1.0,  0.0,   0.0, 1.0,
     0.5,  0.5,  0.5,    0.0,  1.0,  0.0,   1.0, 1.0,
     0.5,  0.5, -0.5,    0.0,  1.0,  0.0,   1.0, 0.0,
    -0.5,  0.5,  0.5,    0.0,  1.0,  0.0,   0.0, 1.0,
     0.5,  0.5, -0.5,    0.0,  1.0,  0.0,   1.0, 0.0,
    -0.5,  0.5, -0.5,    0.0,  1.0,  0.0,   0.0, 0.0,
    // bottom (-y)
    -0.5, -0.5, -0.5,    0.0, -1.0,  0.0,   0.0, 1.0,
     0.5, -0.5, -0.5,    0.0, -1.0,  0.0,   1.0, 1.0,
     0.5, -0.5,  0.5,    0.0, -1.0,  0.0,   1.0, 0.0,
    -0.5, -0.5, -0.5,    0.0, -1.0,  0.0,   0.0, 1.0,
     0.5, -0.5,  0.5,    0.0, -1.0,  0.0,   1.0, 0.0,
    -0.5, -0.5,  0.5,    0.0, -1.0,  0.0,   0.0, 0.0,
    // front (+z)
    -0.5, -0.5,  0.5,    0.0,  0.0,  1.0,   0.0, 1.0,
     0.5, -0.5,  0.5,    0.0,  0.0,  1.0,   1.0, 1.0,
     0.5,  0.5,  0.5,    0.0,  0.0,  1.0,   1.0, 0.0,
    -0.5, -0.5,  0.5,    0.0,  0.0,  1.0,   0.0, 1.0,
     0.5,  0.5,  0.5,    0.0,  0.0,  1.0,   1.0, 0.0,
    -0.5,  0.5,  0.5,    0.0,  0.0,  1.0,   0.0, 0.0,
    // back (-z)
     0.5, -0.5, -0.5,    0.0,  0.0, -1.0,   0.0, 1.0,
    -0.5, -0.5, -0.5,    0.0,  0.0, -1.0,   1.0, 1.0,
    -0.5,  0.5, -0.5,    0.0,  0.0, -1.0,   1.0, 0.0,
     0.5, -0.5, -0.5,    0.0,  0.0, -1.0,   0.0, 1.0,
    -0.5,  0.5, -0.5,    0.0,  0.0, -1.0,   1.0, 0.0,
     0.5,  0.5, -0.5,    0.0,  0.0, -1.0,   0.0, 0.0,
};

vertex_layout: [3]D3D11_INPUT_ELEMENT_DESC = {
    { .SemanticName = "POSITION", .Format = DXGI_FORMAT.DXGI_FORMAT_R32G32B32_FLOAT, .AlignedByteOffset = 0 },
    { .SemanticName = "NORMAL", .Format = DXGI_FORMAT.DXGI_FORMAT_R32G32B32_FLOAT, .AlignedByteOffset = 12 },
    { .SemanticName = "TEXCOORD", .Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, .AlignedByteOffset = 24 },
};

// a floor: position, normal, texture coordinate (repeated 2 times)
plane_vertices: [48]f32 = {
    -5.0, -0.5, 5.0,   0.0, 1.0, 0.0,   0.0, 2.0,
    5.0, -0.5, 5.0,   0.0, 1.0, 0.0,   2.0, 2.0,
    5.0, -0.5, -5.0,   0.0, 1.0, 0.0,   2.0, 0.0,
    -5.0, -0.5, 5.0,   0.0, 1.0, 0.0,   0.0, 2.0,
    5.0, -0.5, -5.0,   0.0, 1.0, 0.0,   2.0, 0.0,
    -5.0, -0.5, -5.0,   0.0, 1.0, 0.0,   0.0, 0.0,
};

// a 1x1 square standing on y = 0, facing +z: grass and windows
quad_vertices: [48]f32 = {
    -0.5, 0.0, 0.0,   0.0, 0.0, 1.0,   0.0, 1.0,
     0.5, 0.0, 0.0,   0.0, 0.0, 1.0,   1.0, 1.0,
     0.5, 1.0, 0.0,   0.0, 0.0, 1.0,   1.0, 0.0,
    -0.5, 0.0, 0.0,   0.0, 0.0, 1.0,   0.0, 1.0,
     0.5, 1.0, 0.0,   0.0, 0.0, 1.0,   1.0, 0.0,
    -0.5, 1.0, 0.0,   0.0, 0.0, 1.0,   0.0, 0.0,
};

window_positions: [5]vec3 = {
    { .x = -1.5, .y = -0.5, .z = -0.48 },
    { .x = 1.5, .y = -0.5, .z = 0.51 },
    { .x = 0.0, .y = -0.5, .z = 0.7 },
    { .x = -0.3, .y = -0.5, .z = -2.3 },
    { .x = 0.5, .y = -0.5, .z = -0.6 },
};

grass_positions: [4]vec3 = {
    { .x = -2.2, .y = -0.5, .z = 0.4 },
    { .x = 2.6, .y = -0.5, .z = 1.2 },
    { .x = 0.6, .y = -0.5, .z = 1.4 },
    { .x = -0.6, .y = -0.5, .z = -1.8 },
};

// Mirrors `cbuffer transforms` in the shaders, register for register.
transforms: struct = {
    model: mat4;
    normal_matrix: mat4;     // inverse transpose of model, for normals
    view: mat4;
    projection: mat4;
    view_position: vec3;     // the eye, in world space
    pad: f32;
}

// camera
cam: camera = {};
last_cursor: POINT = {};
scroll: f32 = 0.0;

// timing
delta_time: f32 = 0.0;
last_frame: f32 = 0.0;
perf_frequency: i64 = 0;
perf_start: i64 = 0;

// Seconds since the first call: what glfwGetTime is to LearnOpenGL.
get_time: proc() -> f32 = {
    now: LARGE_INTEGER = {};
    if (perf_frequency == 0) {
        QueryPerformanceFrequency(now&);
        perf_frequency = now.QuadPart;
        QueryPerformanceCounter(now&);
        perf_start = now.QuadPart;
    }
    QueryPerformanceCounter(now&);
    return cast(now.QuadPart - perf_start, f32) / cast(perf_frequency, f32);
}

// win32: every event for the window (keys, mouse, closing) arrives here
// ---------------------------------------------------------------------
window_proc: proc[callconv(CALLBACK)](hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM) -> LRESULT = {
    if (msg == WM_DESTROY) {
        PostQuitMessage(0);
        return 0;
    }
    if (msg == WM_MOUSEWHEEL) {
        // The high word of wparam is the distance turned, 120 per notch.
        scroll += cast(cast((wparam shr 16) & 0xffff, i16), f32) / 120.0;
        return 0;
    }
    return DefWindowProcA(hwnd, msg, wparam, lparam);
}

// 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.
key_down: proc(key: i32) -> b32 = {
    return GetAsyncKeyState(key) < 0;
}

process_input: proc(window: HWND) -> void = {
    cursor: POINT = {};
    GetCursorPos(cursor&);
    // 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) {
        last_cursor = cursor;
        scroll = 0.0;
        return;
    }
    if (key_down(VK_ESCAPE)) { DestroyWindow(window); }
    if (key_down('W')) { camera_process_keyboard(cam&, camera_movement.forward, delta_time); }
    if (key_down('S')) { camera_process_keyboard(cam&, camera_movement.backward, delta_time); }
    if (key_down('A')) { camera_process_keyboard(cam&, camera_movement.left, delta_time); }
    if (key_down('D')) { camera_process_keyboard(cam&, camera_movement.right, delta_time); }
    if (key_down(VK_SPACE)) { camera_process_keyboard(cam&, camera_movement.up, delta_time); }
    if (key_down(VK_SHIFT)) { camera_process_keyboard(cam&, camera_movement.down, delta_time); }

    // Hold the right mouse button to look around.
    if (key_down(VK_RBUTTON)) {
        // Screen y grows downwards, so moving the mouse up is a negative dy: pitch up.
        camera_process_mouse(cam&, cast(cursor.x - last_cursor.x, f32), cast(last_cursor.y - cursor.y, f32));
    }
    last_cursor = cursor;
    camera_process_scroll(cam&, scroll);
    scroll = 0.0;
}

// An immutable buffer: filled once at creation, never written by the CPU again.
create_buffer: proc(device: *ID3D11Device, bind: UINT, data: *const void, size: u64) -> *ID3D11Buffer = {
    desc: D3D11_BUFFER_DESC = {
        .ByteWidth = cast(size, UINT),
        .Usage = D3D11_USAGE.D3D11_USAGE_IMMUTABLE,
        .BindFlags = bind,
    };
    init: D3D11_SUBRESOURCE_DATA = { .pSysMem = data };
    buffer: *ID3D11Buffer = null;
    ID3D11Device_CreateBuffer(device, desc&, init&, buffer&);
    return buffer;
}

// A constant buffer the CPU rewrites, usually every frame. Its size must be a
// multiple of 16 bytes, the size of one HLSL register.
create_constant_buffer: proc(device: *ID3D11Device, size: u64) -> *ID3D11Buffer = {
    desc: D3D11_BUFFER_DESC = {
        .ByteWidth = cast(size, UINT),
        .Usage = D3D11_USAGE.D3D11_USAGE_DYNAMIC,
        .BindFlags = D3D11_BIND_CONSTANT_BUFFER,
        .CPUAccessFlags = D3D11_CPU_ACCESS_WRITE,
    };
    buffer: *ID3D11Buffer = null;
    ID3D11Device_CreateBuffer(device, desc&, null, buffer&);
    return buffer;
}

// WRITE_DISCARD hands back fresh memory, so the GPU never waits on the CPU.
update_buffer: proc(context: *ID3D11DeviceContext, buffer: *ID3D11Buffer, data: *const void, size: u64) -> void = {
    mapped: D3D11_MAPPED_SUBRESOURCE = {};
    ID3D11DeviceContext_Map(context, cast(buffer, *ID3D11Resource), 0, D3D11_MAP.D3D11_MAP_WRITE_DISCARD, 0, mapped&);
    memcpy(mapped.pData, data, size);
    ID3D11DeviceContext_Unmap(context, cast(buffer, *ID3D11Resource), 0);
}

// The same buffer in the same register for the vertex, geometry and pixel stages.
bind_constants: proc(context: *ID3D11DeviceContext, slot: UINT, buffer: *ID3D11Buffer) -> void = {
    buffers: [1]*ID3D11Buffer = { buffer };
    ID3D11DeviceContext_VSSetConstantBuffers(context, slot, 1, buffers[0]&);
    ID3D11DeviceContext_GSSetConstantBuffers(context, slot, 1, buffers[0]&);
    ID3D11DeviceContext_PSSetConstantBuffers(context, slot, 1, buffers[0]&);
}

// utility function for loading a 2D texture from file, with a full mip chain
// --------------------------------------------------------------------------
// `srgb`: the file stores colours, gamma encoded, and sampling should decode
// them to linear. Data such as normals or roughness is linear already.
load_texture: proc(device: *ID3D11Device, context: *ID3D11DeviceContext, path: [*]const char, srgb: b32) -> *ID3D11ShaderResourceView = {
    img: image = image_load(path);
    if (img.pixels == null) {
        printf("Texture failed to load at path: %s\n", path);
        exit(1);
    }
    desc: D3D11_TEXTURE2D_DESC = {
        .Width = cast(img.width, UINT),
        .Height = cast(img.height, UINT),
        .MipLevels = 0,   // the whole chain
        .ArraySize = 1,
        .Format = srgb ? DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB : DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM,
        .SampleDesc = { .Count = 1 },
        // Generating mips draws into the texture, so it is a render target too.
        .BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET,
        .MiscFlags = D3D11_RESOURCE_MISC_GENERATE_MIPS,
    };
    texture: *ID3D11Texture2D = null;
    srv: *ID3D11ShaderResourceView = null;
    ID3D11Device_CreateTexture2D(device, desc&, null, texture&);
    ID3D11Device_CreateShaderResourceView(device, cast(texture, *ID3D11Resource), null, srv&);
    ID3D11DeviceContext_UpdateSubresource(context, cast(texture, *ID3D11Resource), 0, null, img.pixels, cast(img.width * 4, UINT), 0);
    ID3D11DeviceContext_GenerateMips(context, srv);
    return srv;
}

bind_texture: proc(context: *ID3D11DeviceContext, slot: UINT, srv: *ID3D11ShaderResourceView) -> void = {
    views: [1]*ID3D11ShaderResourceView = { srv };
    ID3D11DeviceContext_PSSetShaderResources(context, slot, 1, views[0]&);
}

// Draws `count` vertices of `buffer` (32-byte vertices) with `model`.
draw_vertices: proc(context: *ID3D11DeviceContext, constants: *ID3D11Buffer, t: *transforms, buffer: *ID3D11Buffer, count: UINT, model: mat4) -> void = {
    stride: UINT = 32;
    offset: UINT = 0;
    ID3D11DeviceContext_IASetVertexBuffers(context, 0, 1, buffer&, stride&, offset&);
    t[0].model = model;
    t[0].normal_matrix = m4_normal_matrix(model);
    update_buffer(context, constants, t, sizeof(transforms));
    ID3D11DeviceContext_Draw(context, count, 0);
}

main: proc() -> i32 = {
    // win32: register a window class and create the window
    // ----------------------------------------------------
    SetProcessDPIAware();
    instance: HMODULE = GetModuleHandleA(null);
    wc: WNDCLASSA = {
        .lpfnWndProc = window_proc,
        .hInstance = instance,
        .hCursor = LoadCursorA(null, IDC_ARROW),
        .lpszClassName = "LearnD3D11",
    };
    RegisterClassA(wc&);
    // A fixed size: the swap chain and every render target keep the size they
    // were created with, so nothing needs rebuilding on resize.
    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;
    }

    // d3d11: create the device, its immediate context and a swap chain
    // ----------------------------------------------------------------
    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,
    };
    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;
    }
    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&);

    // A depth buffer the size of the window: 24 bits of depth, 8 of stencil.
    depth_desc: D3D11_TEXTURE2D_DESC = {
        .Width = cast(scr_width, UINT),
        .Height = cast(scr_height, UINT),
        .MipLevels = 1,
        .ArraySize = 1,
        .Format = DXGI_FORMAT.DXGI_FORMAT_D24_UNORM_S8_UINT,
        .SampleDesc = { .Count = 1 },
        .BindFlags = D3D11_BIND_DEPTH_STENCIL,
    };
    depth_texture: *ID3D11Texture2D = null;
    depth_dsv: *ID3D11DepthStencilView = null;
    ID3D11Device_CreateTexture2D(device, depth_desc&, null, depth_texture&);
    ID3D11Device_CreateDepthStencilView(device, cast(depth_texture, *ID3D11Resource), null, depth_dsv&);

    // configure global d3d11 state
    // ----------------------------
    // GL's conventions rather than D3D's: counter-clockwise triangles face the
    // viewer, and nothing is culled.
    raster_desc: D3D11_RASTERIZER_DESC = {
        .FillMode = D3D11_FILL_MODE.D3D11_FILL_SOLID,
        .CullMode = D3D11_CULL_MODE.D3D11_CULL_NONE,
        .FrontCounterClockwise = 1,
        .DepthClipEnable = 1,
    };
    raster_state: *ID3D11RasterizerState = null;
    ID3D11Device_CreateRasterizerState(device, raster_desc&, raster_state&);
    ID3D11DeviceContext_RSSetState(context, raster_state);
    viewport: D3D11_VIEWPORT = {
        .Width = cast(scr_width, f32),
        .Height = cast(scr_height, f32),
        .MaxDepth = 1.0,
    };

    // A sampler: how textures are filtered and what happens past their edges.
    sampler_desc: D3D11_SAMPLER_DESC = {
        .Filter = D3D11_FILTER.D3D11_FILTER_ANISOTROPIC,
        .AddressU = D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP,
        .AddressV = D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP,
        .AddressW = D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP,
        .MaxAnisotropy = 8,
        .MaxLOD = D3D11_FLOAT32_MAX,
    };
    sampler: *ID3D11SamplerState = null;
    ID3D11Device_CreateSamplerState(device, sampler_desc&, sampler&);
    ID3D11DeviceContext_PSSetSamplers(context, 0, 1, sampler&);

    // A second sampler, clamping at the edges, for the grass and windows.
    clamp_desc: D3D11_SAMPLER_DESC = sampler_desc;
    clamp_desc.AddressU = D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_CLAMP;
    clamp_desc.AddressV = D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_CLAMP;
    clamp_sampler: *ID3D11SamplerState = null;
    ID3D11Device_CreateSamplerState(device, clamp_desc&, clamp_sampler&);

    // out = src * src_alpha + dst * (1 - src_alpha)
    blend_desc: D3D11_BLEND_DESC = {
        .RenderTarget = {
            {
                .BlendEnable = 1,
                .SrcBlend = D3D11_BLEND.D3D11_BLEND_SRC_ALPHA,
                .DestBlend = D3D11_BLEND.D3D11_BLEND_INV_SRC_ALPHA,
                .BlendOp = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD,
                .SrcBlendAlpha = D3D11_BLEND.D3D11_BLEND_ONE,
                .DestBlendAlpha = D3D11_BLEND.D3D11_BLEND_INV_SRC_ALPHA,
                .BlendOpAlpha = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD,
                .RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL,
            },
        },
    };
    alpha_blend: *ID3D11BlendState = null;
    ID3D11Device_CreateBlendState(device, blend_desc&, alpha_blend&);

    // build and compile shaders
    // -------------------------
    s: shader = shader_create(device, filesystem_path("src/4.advanced_d3d11/3.1.blending/blending.hlsl"), vertex_layout);

    // set up vertex data and textures
    // -------------------------------
    cube_vb: *ID3D11Buffer = create_buffer(device, D3D11_BIND_VERTEX_BUFFER, cube_vertices[0]&, sizeof(cube_vertices));
    plane_vb: *ID3D11Buffer = create_buffer(device, D3D11_BIND_VERTEX_BUFFER, plane_vertices[0]&, sizeof(plane_vertices));
    transforms_buffer: *ID3D11Buffer = create_constant_buffer(device, sizeof(transforms));
    cube_texture: *ID3D11ShaderResourceView = load_texture(device, context, filesystem_path("resources/textures/marble.jpg"), 0);
    floor_texture: *ID3D11ShaderResourceView = load_texture(device, context, filesystem_path("resources/textures/metal.png"), 0);
    cam = camera_create(v3(-1.0, 1.0, 4.0), -75.0, -15.0);
    quad_vb: *ID3D11Buffer = create_buffer(device, D3D11_BIND_VERTEX_BUFFER, quad_vertices[0]&, sizeof(quad_vertices));
    grass: *ID3D11ShaderResourceView = load_texture(device, context, filesystem_path("resources/textures/grass.png"), 0);
    glass: *ID3D11ShaderResourceView = load_texture(device, context, filesystem_path("resources/textures/window.png"), 0);
    cam = camera_create(v3(0.0, 0.5, 4.5), -90.0, -10.0);

    // render loop
    // -----------
    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&);
        }

        // per-frame time logic
        // --------------------
        current_frame: f32 = get_time();
        delta_time = current_frame - last_frame;
        last_frame = current_frame;

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

        // render
        // ------
        clear_color: [4]f32 = { 0.1, 0.1, 0.1, 1.0 };
        ID3D11DeviceContext_OMSetRenderTargets(context, 1, backbuffer_rtv&, depth_dsv);
        ID3D11DeviceContext_RSSetViewports(context, 1, viewport&);
        ID3D11DeviceContext_ClearRenderTargetView(context, backbuffer_rtv, clear_color[0]&);
        ID3D11DeviceContext_ClearDepthStencilView(context, depth_dsv, D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0, 0);
        ID3D11DeviceContext_IASetPrimitiveTopology(context, D3D11_PRIMITIVE_TOPOLOGY.D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
        bind_constants(context, 0, transforms_buffer);
        t: transforms = {
            .view = camera_view(cam&),
            .projection = m4_perspective(radians(cam.zoom), cast(scr_width, f32) / cast(scr_height, f32), 0.1, 100.0),
            .view_position = cam.position,
        };

        shader_use(context, s&);
        bind_texture(context, 0, floor_texture);
        draw_vertices(context, transforms_buffer, t&, plane_vb, 6, m4_identity());
        bind_texture(context, 0, cube_texture);
        draw_vertices(context, transforms_buffer, t&, cube_vb, 36, m4_translate(v3(-1.0, 0.0, -1.0)));
        draw_vertices(context, transforms_buffer, t&, cube_vb, 36, m4_translate(v3(2.0, 0.0, 0.0)));

        ID3D11DeviceContext_PSSetSamplers(context, 0, 1, clamp_sampler&);
        bind_texture(context, 0, grass);
        for (i: u32 = 0; i < 4; i += 1) { draw_vertices(context, transforms_buffer, t&, quad_vb, 6, m4_translate(grass_positions[i])); }

        // Farthest first. Five windows: a selection sort is plenty.
        order: [5]u32 = { 0, 1, 2, 3, 4 };
        for (i: u32 = 0; i < 5; i += 1) {
            for (j: u32 = i + 1; j < 5; j += 1) {
                di: f32 = v3_length(v3_sub(window_positions[order[i]], cam.position));
                dj: f32 = v3_length(v3_sub(window_positions[order[j]], cam.position));
                if (dj > di) {
                    swap: u32 = order[i];
                    order[i] = order[j];
                    order[j] = swap;
                }
            }
        }
        ID3D11DeviceContext_OMSetBlendState(context, alpha_blend, null, 0xffffffff);
        bind_texture(context, 0, glass);
        for (i: u32 = 0; i < 5; i += 1) { draw_vertices(context, transforms_buffer, t&, quad_vb, 6, m4_translate(window_positions[order[i]])); }
        ID3D11DeviceContext_OMSetBlendState(context, null, null, 0xffffffff);
        ID3D11DeviceContext_PSSetSamplers(context, 0, 1, sampler&);

        // dxgi: show the image just drawn, waiting for the next vertical blank
        // --------------------------------------------------------------------
        IDXGISwapChain_Present(swap_chain, 1, 0);
    }
    return 0;
}
Part of rin-dx11, LearnOpenGL's lessons rewritten for Direct3D 11 and rin. Licensed CC BY-NC 4.0, after LearnOpenGL by Joey de Vries.