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.