Welcome to ZigFall!

1px Shapes with LINESTRIP with MyDebugBatch

I decided to use LINESTRIP for drawing basic shape primitives for debugging purposes. It will be enough for our purposes for now. LINESTRIP draws lines for vertices like this: 0-1 2-3 4-5. So all we do is pushing vertices to draw lines like this. Let’s get started with the setup.

MyDebugBatch

Here’s the full struct that renders shapes with batching:


const CameraConstants = extern struct {
    view_projection: [16]f32,
};

const DebugVertex = extern struct {
    position: [2]f32,
    color: [4]f32,
};

pub const MyDebugDraw = struct {
    context: *ID3D11DeviceContext,

    vertex_buffer: *ID3D11Buffer,
    cbuffer: *ID3D11Buffer,

    stride: u32 = @sizeOf(DebugVertex),
    vb_offset: u32 = 0,
    vertex_count: u32 = 0,

    mapped: D3D11_MAPPED_SUBRESOURCE = undefined,

    debug_vs: *ID3D11VertexShader,
    debug_ps: *ID3D11PixelShader,

    input_layout: *ID3D11InputLayout,

    fn deinit(self: *Self) void {
        _ = self.vertex_buffer.IUnknown.Release();
        _ = self.cbuffer.IUnknown.Release();

        _ = self.debug_vs.IUnknown.Release();
        _ = self.debug_ps.IUnknown.Release();

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

    const Self = @This();
    fn init(device: *ID3D11Device, context: *ID3D11DeviceContext) !Self {
        var debug_vertex_shader: *ID3D11VertexShader = undefined;
        var hr = device.CreateVertexShader(debug_vs_bytecode, debug_vs_bytecode.len, null, @ptrCast(&debug_vertex_shader));
        if (hr != HRESULT.S_OK) return error.CreateDebugVertexShaderFailed;
        errdefer _ = debug_vertex_shader.IUnknown.Release();

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

        const cbuffer_desc = D3D11_BUFFER_DESC{
            .ByteWidth = @sizeOf(CameraConstants),
            .Usage = .DYNAMIC,
            .BindFlags = .{ .CONSTANT_BUFFER = 1 },
            .CPUAccessFlags = .{ .WRITE = 1 },
            .MiscFlags = .{},
            .StructureByteStride = 0,
        };

        var cbuffer: *ID3D11Buffer = undefined;
        hr = device.CreateBuffer(&cbuffer_desc, null, @ptrCast(&cbuffer));
        if (hr != HRESULT.S_OK) return error.CreateCBufferFailed;
        errdefer _ = cbuffer.IUnknown.Release();

        var vertex_desc = D3D11_BUFFER_DESC{
            .ByteWidth = @sizeOf(DebugVertex) * 4 * MAX_SPRITES_PER_BATCH,
            .Usage = D3D11_USAGE_DYNAMIC,
            .BindFlags = D3D11_BIND_VERTEX_BUFFER,
            .CPUAccessFlags = .{ .WRITE = 1 },
            .MiscFlags = .{},
            .StructureByteStride = 0,
        };

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

        const input_element_descs = [_]D3D11_INPUT_ELEMENT_DESC{
            .{
                .SemanticName = "POSITION",
                .SemanticIndex = 0,
                .Format = DXGI_FORMAT_R32G32_FLOAT,
                .InputSlot = 0,
                .AlignedByteOffset = 0,
                .InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA,
                .InstanceDataStepRate = 0,
            },
            .{
                .SemanticName = "COLOR",
                .SemanticIndex = 0,
                .Format = DXGI_FORMAT_R32G32B32A32_FLOAT,
                .InputSlot = 0,
                .AlignedByteOffset = 8, // 2 floats * 4 bytes = offset past pos
                .InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA,
                .InstanceDataStepRate = 0,
            },
        };

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

        return .{
            .vertex_buffer = vertex_buffer,
            .cbuffer = cbuffer,
            .context = context,
            .debug_vs = debug_vertex_shader,
            .debug_ps = debug_pixel_shader,
            .input_layout = input_layout,
        };
    }

    fn SetCBuffer(self: Self, camera: Camera) !void {
        var mapped: D3D11_MAPPED_SUBRESOURCE = undefined;
        const hr = self.context.Map(
            @ptrCast(self.cbuffer),
            0,
            D3D11_MAP_WRITE_DISCARD,
            0,
            &mapped,
        );
        if (hr != HRESULT.S_OK) return error.MapFailed;
        const dest: *CameraConstants = @ptrCast(@alignCast(mapped.pData));
        dest.* = CameraConstants{ .view_projection = camera.viewProjectionMatrix(game_width, game_height) };
        self.context.Unmap(@ptrCast(self.cbuffer), 0);

        var pp_cbuffer = [_]?*ID3D11Buffer{self.cbuffer};
        //const pp_cbuffer: ?[*]?*ID3D11SamplerState = &pp_raw_c_buffer;
        self.context.VSSetConstantBuffers(0, 1, @ptrCast(&pp_cbuffer));
    }

    pub fn beginBatch(self: *Self) !void {
        const hr = self.context.Map(
            @ptrCast(self.vertex_buffer),
            0,
            D3D11_MAP_WRITE_DISCARD,
            0,
            &self.mapped,
        );
        if (hr != HRESULT.S_OK) return error.MapFailed;
        self.vertex_count = 0;
    }

    pub fn flush(self: *Self, camera: Camera) !void {
        if (self.vertex_count == 0) return;

        self.context.Unmap(@ptrCast(self.vertex_buffer), 0);

        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.IASetInputLayout(self.input_layout);
        self.context.IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINELIST);

        self.context.VSSetShader(self.debug_vs, null, 0);
        self.context.PSSetShader(self.debug_ps, null, 0);

        try self.SetCBuffer(camera);

        self.context.Draw(self.vertex_count, 0);

        self.vertex_count = 0;
    }

    pub fn drawLine(self: *Self, p0: [2]f32, p1: [2]f32, color: [4]f32) void {
        const dst: [*]DebugVertex = @ptrCast(@alignCast(self.mapped.pData));
        const base = self.vertex_count;

        dst[base + 0] = DebugVertex{ .position = p0, .color = color };
        dst[base + 1] = DebugVertex{ .position = p1, .color = color };

        self.vertex_count += 2;
    }
    pub fn drawRect(self: *Self, min: [2]f32, max: [2]f32, color: [4]f32) void {
        const dst: [*]DebugVertex = @ptrCast(@alignCast(self.mapped.pData));
        const base = self.vertex_count;

        const corners = [_][2]f32{
            .{ min[0], min[1] },
            .{ max[0], min[1] },
            .{ max[0], max[1] },
            .{ min[0], max[1] },
        };

        var i: u32 = 0;
        while (i < 4) : (i += 1) {
            dst[base + i * 2 + 0] = DebugVertex{ .position = corners[i], .color = color };
            dst[base + i * 2 + 1] = DebugVertex{ .position = corners[(i + 1) % 4], .color = color };
        }

        self.vertex_count += 8;
    }
    pub fn drawCircle(self: *Self, center: [2]f32, radius: f32, color: [4]f32) void {
        const segment_count: u8 = 8;
        const dst: [*]DebugVertex = @ptrCast(@alignCast(self.mapped.pData));
        const base = self.vertex_count;

        var i: u32 = 0;
        while (i < segment_count) : (i += 1) {
            const theta0 = @as(f32, @floatFromInt(i)) / @as(f32, @floatFromInt(segment_count)) * std.math.tau;
            const theta1 = @as(f32, @floatFromInt(i + 1)) / @as(f32, @floatFromInt(segment_count)) * std.math.tau;

            dst[base + i * 2 + 0] = DebugVertex{
                .position = .{
                    center[0] + radius * @cos(theta0),
                    center[1] + radius * @sin(theta0),
                },
                .color = color,
            };

            dst[base + i * 2 + 1] = DebugVertex{
                .position = .{
                    center[0] + radius * @cos(theta1),
                    center[1] + radius * @sin(theta1),
                },
                .color = color,
            };
        }

        self.vertex_count += segment_count * 2;
    }
};

and the corresponding shader:

struct VSInput
{
    float2 pos   : POSITION;
    float4 color : COLOR0;
};

struct PSInput
{
    float4 pos   : SV_POSITION;
    float4 color : COLOR0;
};

cbuffer CameraBuffer : register(b0) {
    matrix view_projection;
};

PSInput VSMain(VSInput input)
{
    PSInput output;
    output.pos = mul(view_projection, float4(input.pos, 0.0, 1.0));
    output.color = input.color;
    return output;
}

float4 PSMain(PSInput input) : SV_TARGET
{
    return input.color;
}

and finally the Camera struct that calculates the viewProjection matrix:

const Self = @This();
position: [2]f32,
zoom: f32,
// rotation: f32

pub fn init(x: f32, y: f32) Self {
    return .{ .position = .{ x, y }, .zoom = 1 };
}

pub fn viewProjectionMatrix(self: Self, viewport_width: f32, viewport_height: f32) [16]f32 {
    // world units visible across the viewport at current zoom
    const view_w = viewport_width / self.zoom;
    const view_h = viewport_height / self.zoom;

    const sx = 2.0 / view_w;
    const sy = -2.0 / view_h;

    // translate so camera.position is centered in the view
    const tx = -self.position[0] * sx;
    const ty = -self.position[1] * sy;

    return [16]f32{
        sx,  0.0, 0.0, 0.0,
        0.0, sy,  0.0, 0.0,
        0.0, 0.0, 1.0, 0.0,
        tx,  ty,  0.0, 1.0,
    };
}

Organize MyDirectXContext into MyBatchDraw

In our initial startup, I had no idea how DirectX works, with some familiarity, I realized parts of MyDirectXContext actually belong to MyBatchDraw. I realized this after debugging how shape renderer was glitching.

I will share some abstracted rendering logic, but you shall get the idea:

    pub fn render(self: *Self, alpha: f32) void {
        self.platform.beginDraw();

        self.platform.beginSpriteDraw();
        scn.renderScene(self.platform.spriteBatch(), &self.scene, alpha);
        self.platform.endSpriteDraw();

        self.platform.beginDebugDraw();
        scn.renderDebug(self.platform.debugBatch(), &self.scene);
        self.platform.endDebugDraw(self.scene.camera);

        self.platform.endDraw();
    }

Enjoy our rendering pipeline v0.4.0.

Textures and Basic Shapes
Textures and Basic Shapes

Interfaces in Zig and Platform Abstractions
Watching file changes and handling keyboard input