Welcome to ZigFall!

Fixed Resolution Responsive Game View for Resizable Window

We rendered our first triangle in previous blog post. Now we want to fix our game resolution to something like 640x360 and rescale the game to fit the window resolution. In this way, no matter what the window size, we will always calculate the positions for where we are rendering on the screen relative to 640x360 fixed resolution. But there are 2 catches.

So that’s the tradeoff, I will show you the integer multiple version first, then there is a small refactoring to make it work with fractional scaling.

Direct3D11 Swap Chain, Render Target, Buffers, Shader Resources, Render States ..

All of these we will setup and use in a giant block of struct. But to get started, I will tell you one gotc’ha about ownership of resources:

Take a look at this code:

        var back_buffer: *ID3D11Texture2D = undefined;
        var backbuffer_rtv: *ID3D11RenderTargetView = undefined;
        {
            hr = swap_chain.GetBuffer(0, IID_ID3D11Texture2D, @ptrCast(&back_buffer));
            if (hr != HRESULT.S_OK) return error.GetBackBufferFailed;
            defer _ = back_buffer.IUnknown.Release();

            hr = device.CreateRenderTargetView(@ptrCast(back_buffer), null, @ptrCast(&backbuffer_rtv));
            if (hr != HRESULT.S_OK) return error.CreateRTVFailed;
            errdefer _ = backbuffer_rtv.IUnknown.Release();
        }

We first create a temporary back_buffer with GetBuffer, then we pass it to CreateRenderTargetView to create a backbuffer_rtv. But we have to defer _ = back_buffer.IUnknown.Release();, such that back_buffer is not needed anymore, it is used to create the Render Target View we will hold on to and back_buffer not needed anymore.

Now we are doing 2 things here:

I will show the blit.hlsl here, your homework is to setup the compilation in build.zig yourself exactly same as triangle.hlsl we showed on previous post.

// Draws a single fullscreen triangle from SV_VertexID (no vertex/index
// buffer needed) and samples the offscreen game render target onto it.
// The letterbox/pillarbox centering and integer scaling are done by how
// main.zig sets the viewport before this draw, not by anything in here.

Texture2D game_tex : register(t0);
SamplerState game_sampler : register(s0);

struct VSOutput
{
    float4 pos : SV_POSITION;
    float2 uv  : TEXCOORD0;
};

VSOutput VSMain(uint vertex_id : SV_VertexID)
{
    VSOutput output;
    output.uv = float2((vertex_id << 1) & 2, vertex_id & 2);
    output.pos = float4(output.uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
    return output;
}

float4 PSMain(VSOutput input) : SV_TARGET
{
    return game_tex.Sample(game_sampler, input.uv);
}

To be honest, I had no idea how DirectX works, and a lot of back and forth vibe-coding involved while trying to get this right. I will just dump the full working struct here as is. It has all the debugging prints and such, but it is complete and working as expected. It is your homework to clean this up and adjust.


const MyDirectXContext = struct {
    backbuffer_rtv: ?*ID3D11RenderTargetView,

    game_rtv: *ID3D11RenderTargetView,

    device: *ID3D11Device,
    context: *ID3D11DeviceContext,
    swap_chain: *IDXGISwapChain,

    input_layout: *ID3D11InputLayout,
    vertex_buffer: *ID3D11Buffer,

    vertex_shader: *ID3D11VertexShader,
    pixel_shader: *ID3D11PixelShader,

    blit_vertex_shader: *ID3D11VertexShader,
    blit_pixel_shader: *ID3D11PixelShader,

    stride: u32,
    vb_offset: u32,
    world_clear_color: [4]f32,
    letterbox_color: [4]f32,
    null_srv: ?*ID3D11ShaderResourceView,

    game_srv: *ID3D11ShaderResourceView,

    sampler: *ID3D11SamplerState,

    rasterizer_state: *ID3D11RasterizerState,

    hwnd: HWND,

    game_viewport: D3D11_VIEWPORT,

    const Self = @This();
    fn deinit(self: *Self) void {
        self.context.ClearState();

        _ = self.sampler.IUnknown.Release();
        _ = self.game_rtv.IUnknown.Release();

        _ = self.game_srv.IUnknown.Release();

        _ = self.input_layout.IUnknown.Release();
        _ = self.vertex_buffer.IUnknown.Release();

        _ = self.vertex_shader.IUnknown.Release();
        _ = self.pixel_shader.IUnknown.Release();

        _ = self.blit_vertex_shader.IUnknown.Release();
        _ = self.blit_pixel_shader.IUnknown.Release();

        _ = self.rasterizer_state.IUnknown.Release();

        if (self.backbuffer_rtv) |backbuffer_rtv|
            _ = backbuffer_rtv.IUnknown.Release();

        _ = self.device.IUnknown.Release();
        _ = self.context.IUnknown.Release();
        _ = self.swap_chain.IUnknown.Release();
    }

    fn init(hwnd: HWND) !MyDirectXContext {
        const swap_chain_desc = DXGI_SWAP_CHAIN_DESC{
            .BufferDesc = .{
                .Width = @intCast(client_width),
                .Height = @intCast(client_height),
                .Format = DXGI_FORMAT_R8G8B8A8_UNORM,
                .RefreshRate = .{ .Numerator = 60, .Denominator = 1 },
                .ScanlineOrdering = .UNSPECIFIED,
                .Scaling = .UNSPECIFIED,
            },
            .SampleDesc = .{ .Count = 1, .Quality = 0 },
            .BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT,
            .BufferCount = 1,
            .OutputWindow = hwnd,
            .Windowed = TRUE,
            .SwapEffect = DXGI_SWAP_EFFECT_DISCARD,
            .Flags = 0,
        };

        var swap_chain: *IDXGISwapChain = undefined;
        var device: *ID3D11Device = undefined;
        var context: *ID3D11DeviceContext = undefined;
        var feature_level: D3D_FEATURE_LEVEL = undefined;

        const create_flags: D3D11_CREATE_DEVICE_FLAG = if (@import("builtin").mode == .Debug)
            D3D11_CREATE_DEVICE_DEBUG
        else
            .{};

        var hr = D3D11CreateDeviceAndSwapChain(
            null, // default adapater
            D3D_DRIVER_TYPE_HARDWARE,
            null,
            create_flags,
            null, // let it pick the highest feature level available
            0,
            D3D11_SDK_VERSION,
            &swap_chain_desc,
            @ptrCast(&swap_chain),
            @ptrCast(&device),
            &feature_level,
            @ptrCast(&context),
        );

        if (hr != HRESULT.S_OK) return error.D3D11DeviceCreationFailed;

        // --- Backbuffer render target view (recreated on every WM_SIZE) ---

        var back_buffer: *ID3D11Texture2D = undefined;
        var backbuffer_rtv: *ID3D11RenderTargetView = undefined;
        {
            hr = swap_chain.GetBuffer(0, IID_ID3D11Texture2D, @ptrCast(&back_buffer));
            if (hr != HRESULT.S_OK) return error.GetBackBufferFailed;
            defer _ = back_buffer.IUnknown.Release();

            hr = device.CreateRenderTargetView(@ptrCast(back_buffer), null, @ptrCast(&backbuffer_rtv));
            if (hr != HRESULT.S_OK) return error.CreateRTVFailed;
            errdefer _ = backbuffer_rtv.IUnknown.Release();
        }

        // --- Offscreen "game" render target: fixed at game_width x
        // game_height  for the lifetime of the program.
        var game_tex_desc = D3D11_TEXTURE2D_DESC{
            .Width = game_width,
            .Height = game_height,
            .MipLevels = 1,
            .ArraySize = 1,
            .Format = DXGI_FORMAT_R8G8B8A8_UNORM,
            .SampleDesc = .{ .Count = 1, .Quality = 0 },
            .Usage = D3D11_USAGE_DEFAULT,
            .BindFlags = .{ .RENDER_TARGET = 1, .SHADER_RESOURCE = 1 },
            .CPUAccessFlags = .{},
            .MiscFlags = .{},
        };

        var game_tex: *ID3D11Texture2D = undefined;
        hr = device.CreateTexture2D(&game_tex_desc, null, @ptrCast(&game_tex));
        if (hr != HRESULT.S_OK) return error.CreateGameTextureFailed;
        defer _ = game_tex.IUnknown.Release();

        var game_rtv: *ID3D11RenderTargetView = undefined;
        hr = device.CreateRenderTargetView(@ptrCast(game_tex), null, @ptrCast(&game_rtv));
        if (hr != HRESULT.S_OK) return error.CreateGameRTVFailed;
        errdefer _ = game_rtv.IUnknown.Release();

        var game_srv: *ID3D11ShaderResourceView = undefined;
        hr = device.CreateShaderResourceView(@ptrCast(game_tex), null, @ptrCast(&game_srv));
        if (hr != HRESULT.S_OK) return error.CreateGameSRVFailed;
        errdefer _ = game_srv.IUnknown.Release();

        const game_viewport = D3D11_VIEWPORT{
            .Width = game_width,
            .Height = game_height,
            .MinDepth = 0.0,
            .MaxDepth = 1.0,
            .TopLeftX = 0.0,
            .TopLeftY = 0.0,
        };

        var sampler_desc = D3D11_SAMPLER_DESC{
            .Filter = D3D11_FILTER_MIN_MAX_MIP_LINEAR,
            .AddressU = D3D11_TEXTURE_ADDRESS_CLAMP,
            .AddressV = D3D11_TEXTURE_ADDRESS_CLAMP,
            .AddressW = D3D11_TEXTURE_ADDRESS_CLAMP,
            .ComparisonFunc = D3D11_COMPARISON_NEVER,
            .MaxLOD = D3D11_FLOAT32_MAX,
            .MipLODBias = 0,
            .MaxAnisotropy = 0,
            .BorderColor = [4]f32{ 0.0, 0.0, 0.0, 1.0 },
            .MinLOD = 0,
        };

        var sampler: *ID3D11SamplerState = undefined;
        hr = device.CreateSamplerState(&sampler_desc, @ptrCast(&sampler));
        if (hr != HRESULT.S_OK) return error.CreateSamplerFailed;
        errdefer _ = sampler.IUnknown.Release();

        var rasterizer_desc = D3D11_RASTERIZER_DESC{
            .FillMode = D3D11_FILL_SOLID,
            .CullMode = D3D11_CULL_NONE,
            .DepthClipEnable = TRUE,
            .AntialiasedLineEnable = FALSE,
            .DepthBias = FALSE,
            .DepthBiasClamp = FALSE,
            .FrontCounterClockwise = FALSE,
            .MultisampleEnable = FALSE,
            .ScissorEnable = FALSE,
            .SlopeScaledDepthBias = FALSE,
        };

        var rasterizer_state: *ID3D11RasterizerState = undefined;
        hr = device.CreateRasterizerState(&rasterizer_desc, @ptrCast(&rasterizer_state));
        if (hr != HRESULT.S_OK) return error.CreateRasterizerStateFailed;
        errdefer _ = rasterizer_state.IUnknown.Release();

        // Shader additions

        var vertex_shader: *ID3D11VertexShader = undefined;
        hr = device.CreateVertexShader(vs_bytecode, vs_bytecode.len, null, @ptrCast(&vertex_shader));
        if (hr != HRESULT.S_OK) return error.CreateVertexShaderFailed;
        errdefer _ = vertex_shader.IUnknown.Release();

        var pixel_shader: *ID3D11PixelShader = undefined;
        hr = device.CreatePixelShader(ps_bytecode, ps_bytecode.len, null, @ptrCast(&pixel_shader));
        if (hr != HRESULT.S_OK) return error.CreatePixelShaderFailed;
        errdefer _ = pixel_shader.IUnknown.Release();

        const input_element_descs = [_]D3D11_INPUT_ELEMENT_DESC{
            .{
                .SemanticName = "POSITION",
                .SemanticIndex = 0,
                .Format = DXGI_FORMAT_R32G32B32_FLOAT,
                .InputSlot = 0,
                .AlignedByteOffset = 0,
                .InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA,
                .InstanceDataStepRate = 0,
            },
            .{
                .SemanticName = "COLOR",
                .SemanticIndex = 0,
                .Format = DXGI_FORMAT_R32G32B32_FLOAT,
                .InputSlot = 0,
                .AlignedByteOffset = 12, // 3 floats of POSITION
                .InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA,
                .InstanceDataStepRate = 0,
            },
        };

        var input_layout: *ID3D11InputLayout = undefined;
        hr = device.CreateInputLayout(
            &input_element_descs,
            input_element_descs.len,
            vs_bytecode,
            vs_bytecode.len,
            @ptrCast(&input_layout),
        );
        if (hr != HRESULT.S_OK) return error.CreateInputLayoutFailed;
        errdefer _ = input_layout.IUnknown.Release();

        // --- Vertex buffer: one small hardcoded triangle, in NDC space already
        // (no view/projection matrix yet)
        const Vertex = extern struct {
            pos: [3]f32,
            color: [4]f32,
        };

        const vertices = [_]Vertex{
            .{ .pos = .{ 1.0, 1.0, 0.0 }, .color = .{ 1.0, 0.0, 0.0, 1.0 } },
            .{ .pos = .{ 1.0, -1.0, 0.0 }, .color = .{ 0.0, 1.0, 0.0, 1.0 } },
            .{ .pos = .{ -1.0, -1.0, 0.0 }, .color = .{ 0.0, 0.0, 1.0, 1.0 } },
        };

        var buffer_desc = D3D11_BUFFER_DESC{
            .ByteWidth = @sizeOf(@TypeOf(vertices)),
            .Usage = D3D11_USAGE_IMMUTABLE,
            .BindFlags = D3D11_BIND_VERTEX_BUFFER,
            .CPUAccessFlags = .{},
            .MiscFlags = .{},
            .StructureByteStride = 0,
        };

        var init_data = D3D11_SUBRESOURCE_DATA{
            .pSysMem = &vertices,
            .SysMemPitch = 0,
            .SysMemSlicePitch = 0,
        };

        var vertex_buffer: *ID3D11Buffer = undefined;
        hr = device.CreateBuffer(&buffer_desc, &init_data, @ptrCast(&vertex_buffer));
        if (hr != HRESULT.S_OK) return error.CreateVertexBufferFailed;
        errdefer _ = vertex_buffer.IUnknown.Release();

        // --- Blit Shaders (fullscreen triangle, no vertex buffer needed) ---
        var blit_vertex_shader: *ID3D11VertexShader = undefined;
        hr = device.CreateVertexShader(blit_vs_bytecode, blit_vs_bytecode.len, null, @ptrCast(&blit_vertex_shader));
        if (hr != HRESULT.S_OK) return error.CreateBlitVertexShaderFailed;
        errdefer _ = blit_vertex_shader.IUnknown.Release();

        var blit_pixel_shader: *ID3D11PixelShader = undefined;
        hr = device.CreatePixelShader(blit_ps_bytecode, blit_ps_bytecode.len, null, @ptrCast(&blit_pixel_shader));
        if (hr != HRESULT.S_OK) return error.CreateBlitPixelShaderFailed;
        errdefer _ = blit_pixel_shader.IUnknown.Release();

        const stride: u32 = @sizeOf(Vertex);
        const vb_offset: u32 = 0;
        const world_clear_color = [4]f32{ 0.10, 0.10, 0.35, 1.0 }; // dark blue
        const letterbox_color = [4]f32{ 0.0, 0.0, 0.0, 1.0 }; // black bars
        const null_srv: ?*ID3D11ShaderResourceView = null;

        return .{
            .game_viewport = game_viewport,
            .stride = stride,
            .vb_offset = vb_offset,
            .world_clear_color = world_clear_color,
            .letterbox_color = letterbox_color,
            .null_srv = null_srv,

            .game_srv = game_srv,

            .sampler = sampler,

            .rasterizer_state = rasterizer_state,

            .vertex_shader = vertex_shader,
            .pixel_shader = pixel_shader,

            .blit_vertex_shader = blit_vertex_shader,
            .blit_pixel_shader = blit_pixel_shader,

            .hwnd = hwnd,
            .vertex_buffer = vertex_buffer,
            .input_layout = input_layout,

            .backbuffer_rtv = backbuffer_rtv,
            .game_rtv = game_rtv,

            .context = context,
            .device = device,
            .swap_chain = swap_chain,
        };
    }

    fn draw(self: *Self) void {
        var client_rect: RECT = undefined;
        _ = GetClientRect(self.hwnd, &client_rect);
        const win_w = client_rect.right - client_rect.left;
        const win_h = client_rect.bottom - client_rect.top;
        if (win_w <= 0 or win_h <= 0) return; // minimized -- nothing to draw

        // --- Pass 1: render the world into the fixed-size game target ---
        var raw_rtvs = [_]?*ID3D11RenderTargetView{self.game_rtv};
        const rtvs: ?[*]?*ID3D11RenderTargetView = &raw_rtvs;
        self.context.OMSetRenderTargets(1, rtvs, null);
        self.context.RSSetViewports(1, @ptrCast(&self.game_viewport));
        self.context.RSSetState(self.rasterizer_state);
        self.context.ClearRenderTargetView(self.game_rtv, @ptrCast(&self.world_clear_color[0]));

        self.context.IASetInputLayout(self.input_layout);
        self.context.IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

        const strides = &[_]u32{self.stride};
        const vb_offsets = &[_]u32{self.vb_offset};
        self.context.IASetVertexBuffers(0, 1, @ptrCast(&self.vertex_buffer), strides, vb_offsets);

        self.context.VSSetShader(self.vertex_shader, null, 0);
        self.context.PSSetShader(self.pixel_shader, null, 0);
        self.context.Draw(3, 0);

        // -- Pass 2: blit game target onto the backbuffer, integer-scaled
        // centered, with black bars for whatever doesn't divide evenly ---

        var raw_backbuffer_rtvs = [_]?*ID3D11RenderTargetView{self.backbuffer_rtv};
        const backbuffer_rtvs: ?[*]?*ID3D11RenderTargetView = &raw_backbuffer_rtvs;
        self.context.OMSetRenderTargets(1, backbuffer_rtvs, null);
        self.context.ClearRenderTargetView(self.backbuffer_rtv, @ptrCast(&self.letterbox_color[0]));

        const win_w_f: f32 = @floatFromInt(win_w);
        const win_h_f: f32 = @floatFromInt(win_h);
        const game_w_f: f32 = @floatFromInt(game_width);
        const game_h_f: f32 = @floatFromInt(game_height);

        //const scale_x = @divTrunc(win_w, @as(i32, @intCast(game_width)));
        //const scale_y = @divTrunc(win_h, @as(i32, @intCast(game_height)));
        //const scale = @max(1, @min(scale_x, scale_y));

        const scale = @min(win_w_f / game_w_f, win_h_f / game_h_f);

        //const out_w = @as(i32, @intCast(game_width)) * scale;
        //const out_h = @as(i32, @intCast(game_height)) * scale;
        const out_w = game_w_f * scale;
        const out_h = game_h_f * scale;

        //const offset_x = @divTrunc(win_w - out_w, 2);
        //const offset_y = @divTrunc(win_h - out_h, 2);

        const offset_x = (win_w_f - out_w) / 2.0;
        const offset_y = (win_h_f - out_h) / 2.0;

        const Last = struct {
            var w: i32 = -1;
            var h: i32 = -1;
        };
        if (win_w != Last.w or win_h != Last.h) {
            Last.w = win_w;
            Last.h = win_h;
            std.debug.print(
                "client={}x{} scale={} out={}x{} offset={},{}\n",
                .{ win_w, win_h, scale, out_w, out_h, offset_x, offset_y },
            );
        }

        var blit_viewport = D3D11_VIEWPORT{
            .TopLeftX = offset_x,
            .TopLeftY = offset_y,
            .Width = out_w,
            .Height = out_h,
            .MinDepth = 0,
            .MaxDepth = 1.0,
        };

        self.context.RSSetViewports(1, @ptrCast(&blit_viewport));

        self.context.IASetInputLayout(null);
        self.context.IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
        self.context.VSSetShader(self.blit_vertex_shader, null, 0);
        self.context.PSSetShader(self.blit_pixel_shader, null, 0);

        var raw_srv = [_]?*ID3D11ShaderResourceView{self.game_srv};
        const game_srvs: ?[*]?*ID3D11ShaderResourceView = &raw_srv;
        self.context.PSSetShaderResources(0, 1, game_srvs);

        var raw_sampler = [_]?*ID3D11SamplerState{self.sampler};
        const samplers: ?[*]?*ID3D11SamplerState = &raw_sampler;
        self.context.PSSetSamplers(0, 1, samplers);
        self.context.Draw(3, 0);

        // Unbind the SRV so game_tex is free to be used as a render target
        // again next frame -- a resource can't be bound as both at once.

        var null_srv = [_]?*ID3D11ShaderResourceView{self.null_srv};
        const null_srvs: ?[*]?*ID3D11ShaderResourceView = &null_srv;
        self.context.PSSetShaderResources(0, 1, null_srvs);

        _ = self.swap_chain.Present(1, 0); // 1 = vynsc on
    }
};

The second part of the picture is to handle WM_SIZE in our Windows Message Procedure:

fn processWindowMessage(hwnd: HWND, msg: u32, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) LRESULT {
    switch (msg) {
        WM_DESTROY => {
            PostQuitMessage(0);
            return 0;
        },
        WM_SIZE => {
            const SIZE_MINIMIZED: usize = 1;
            if (wParam == SIZE_MINIMIZED) return 0;

            const lp: u32 = @truncate(@as(usize, @bitCast(lParam)));
            const new_width: u32 = lp & 0xFFFF;
            const new_height: u32 = (lp >> 16) & 0xFFFF;
            if (new_width == 0 or new_height == 0) return 0;

            // GWLP_USERDATA is 0 for the WM_SIZE messages Windows
            // sends during CreateWindowExW itself, since that's before
            // SetWindowLongPtrW runs in main() -- nothing to resize yet.
            const user_data = GetWindowLongPtrW(hwnd, GWLP_USERDATA);
            if (user_data == 0) return 0;
            const state: *MyDirectXContext = @ptrFromInt(@as(usize, @bitCast(user_data)));

            state.context.OMSetRenderTargets(0, null, null);

            //state.context.ClearState();
            //state.context.Flush();

            //Drop our reference to the old backbuffer RTV before
            // resizing -- ResizeBuffers fails if anything still
            // references the swap chain's buffers.
            if (state.backbuffer_rtv) |old_rtv| {
                _ = old_rtv.IUnknown.Release();
                state.backbuffer_rtv = null;
            }

            const hr = state.swap_chain.ResizeBuffers(0, new_width, new_height, DXGI_FORMAT_UNKNOWN, 0);
            if (hr != HRESULT.S_OK) {
                std.debug.print("ResizeBuffers failed: hr={}\n", .{hr});

                var debug_iface: ?*IDXGIDebug = null;
                if (!DXGIGetDebugInterface1(0, IID_IDXGIDebug, @ptrCast(&debug_iface)).failed) {
                    if (debug_iface) |dbg| {
                        _ = dbg.ReportLiveObjects(DXGI_DEBUG_ALL, DXGI_DEBUG_RLO_DETAIL);
                        _ = dbg.IUnknown.Release();
                    }
                }

                return 0;
            }

            var back_buffer: *ID3D11Texture2D = undefined;
            if (state.swap_chain.GetBuffer(0, IID_ID3D11Texture2D, @ptrCast(&back_buffer)) == HRESULT.S_OK) {
                defer _ = back_buffer.IUnknown.Release();
                var new_rtv: *ID3D11RenderTargetView = undefined;
                if (!state.device.CreateRenderTargetView(@ptrCast(back_buffer), null, @ptrCast(&new_rtv)).failed) {
                    state.backbuffer_rtv = new_rtv;
                    std.debug.print("RenderTargetSetTo{*}\n", .{new_rtv});
                } else {
                    std.debug.print("CreateRenderTargetView", .{});
                }
            } else {
                std.debug.print("GetBuffer failed after resize", .{});
            }
            return 0;
        },
        else => return DefWindowProcA(hwnd, msg, wParam, lParam),
    }
}

If you have been careful, you might have noticed:

    const user_data = GetWindowLongPtrW(hwnd, GWLP_USERDATA);
    if (user_data == 0) return 0;
    const state: *MyDirectXContext = @ptrFromInt(@as(usize, @bitCast(user_data)));

This is how we get the context of our DirectX struct inside the processWindowMessage, and of course we have to set it first at setup time like this:

    if (hwnd) |hwndV| {
        _ = ShowWindow(hwndV, SW_SHOW);

        var context: MyDirectXContext = try .init(hwndV);
        defer context.deinit();

        _ = SetWindowLongPtrW(hwnd, GWLP_USERDATA, @bitCast(@intFromPtr(&context)));

        try runGameLoop(&context);
    } // else

And that’s it, we now have a responsive game area with fixed resolution view.

To recap; the main source of issue I tackled was this ResizeBuffers was failing:

        const hr = state.swap_chain.ResizeBuffers(0, new_width, new_height, DXGI_FORMAT_UNKNOWN, 0);

due to an unreleased resource in the DirectX state. So I had to release whatever was bounded somewhere. That led me to the gotch’a I told you in the beginning. After that the triangle wasn’t rendering, because I forget to set the vertex buffers like this:

        const strides = &[_]u32{self.stride};
        const vb_offsets = &[_]u32{self.vb_offset};
        self.context.IASetVertexBuffers(0, 1, @ptrCast(&self.vertex_buffer), strides, vb_offsets);

Finally the triangle rendered half the game area, instead of covering the entire area, that was because I incorectly set the game_tex_desc resolution to client resolution instead: (this is the correct version)

    var game_tex_desc = D3D11_TEXTURE2D_DESC{
        .Width = game_width,
        .Height = game_height,

Correction: I actually showed you the fractional scaling first which I intent to keep, but here’s what the integer scaling would look like:

        const scale_x = @divTrunc(win_w, @as(i32, @intCast(game_width)));
        const scale_y = @divTrunc(win_h, @as(i32, @intCast(game_height)));
        const scale = @max(1, @min(scale_x, scale_y));

        const out_w = @as(i32, @intCast(game_width)) * scale;
        const out_h = @as(i32, @intCast(game_height)) * scale;

        const offset_x = @divTrunc(win_w - out_w, 2);
        const offset_y = @divTrunc(win_h - out_h, 2);
Win32 Bindings and Creating First Window
Fullscreen Mode and Draw while Resizing