Skip to content

Instantly share code, notes, and snippets.

@jdmichaud
Last active May 1, 2025 09:05
Show Gist options
  • Save jdmichaud/eb8659052f3f74dbfb2b62ad0bfb7897 to your computer and use it in GitHub Desktop.
Save jdmichaud/eb8659052f3f74dbfb2b62ad0bfb7897 to your computer and use it in GitHub Desktop.
Zig utils

Zig utils

  • clap.zig: Command line argument
  • draw.zig: Provide a canvas context 'like' interface for drawing
  • io-adapter:
    • Keyboard/mouse input + graphic output
    • One adapter for SDL for now
  • misc.zig:
    • Load a file
    • Write an image (pgm or pam)
// A very simple command line argument parser.
//
// The purpose is to be simple, modifiable and easily embeddable. Just
// copy-paste is into your project and that's it.
//
// Usage:
// ```zig
// const args = try std.process.argsAlloc(allocator);
// defer std.process.argsFree(allocator, args);
//
// const parsedArgs = clap.parser(clap.ArgDescriptor{
// .name = "qvm",
// .description = "A QuakeC virtual machine",
// .withHelp = true,
// .version = "0.1.0",
// .expectArgs = &[_][]const u8{ "datfile" },
// .options = &[_]clap.OptionDescription{ .{
// .short = "t",
// .long = "trace",
// .help = "Enable tracing of instructions",
// }, .{
// .short = "e",
// .long = "verbose",
// .help = "Display additional information about the VM",
// }, .{
// .short = "m",
// .long = "memory-size",
// .arg = .{ .name = "memory", .type = []const u8 },
// .help = "Amount of memory to allocate for the VM (-m 12, -m 64K, -m 1M)",
// }, .{
// .short = "j",
// .long = "jump-to",
// .arg = .{ .name = "function", .type = []const u8 },
// .help = "Jump to function on startup",
// }, .{
// .short = "b",
// .long = "bsp-file",
// .arg = .{ .name = "bspfile", .type = []const u8 },
// .help = "Load a BSP file",
// }, .{
// .short = "r",
// .long = "run",
// .help = "Run the event loop (triggering the nextthink timers)",
// } },
// }).parse(args);
//
// const filepath = parsedArgs.arguments.items[0];
// const memsize = if (args.getOption([]const u8, "memory-size")) |memsizeArg| blkinner: {
// const lastChar = memsizeArg[memsizeArg.len - 1];
// break :blkinner switch (lastChar) {
// 'k', 'K' => try std.fmt.parseInt(usize, memsizeArg[0..memsizeArg.len - 1], 10) * 1024,
// 'm', 'M' => try std.fmt.parseInt(usize, memsizeArg[0..memsizeArg.len - 1], 10) * 1024 * 1024,
// else => try std.fmt.parseInt(usize, memsizeArg, 10),
// };
// } else 1024 * 1024 * 1; // 1Mb by default;
// // Create the VM
// var vm = try VM.init(allocator, .{
// .entries = null,
// }, .{
// .trace = parsedArgs.getSwitch("trace"),
// .memsize = memsize,
// .verbose = parsedArgs.getSwitch("verbose"),
// });
// defer vm.deinit();
// ```
// Running with the `--help` option will show:
// ```
// qvm (0.1.0) A QuakeC virtual machine
// Usage: qvm [OPTIONS] datfile
//
// Options:
// -t,--trace Enable tracing of instructions
// -e,--verbose Display additional information about the VM
// -m,--memory-size memory
// Amount of memory to allocate for the VM (-m 12, -m 64K, -m 1M)
// -j,--jump-to function
// Jump to function on startup
// -b,--bsp-file bspfile
// Load a BSP file
// -r,--run Run the event loop (triggering the nextthink timers)
//
// ```
const std = @import("std");
const stdout = std.io.getStdOut().writer();
const stderr = std.io.getStdErr().writer();
pub const OptionDescription = struct {
short: ?[]const u8,
long: []const u8,
arg: ?struct { name: []const u8, type: type } = null,
help: []const u8,
};
pub const ArgDescriptor = struct {
bufferSize: usize = 1024,
name: []const u8,
description: ?[]const u8 = null,
withHelp: bool = true,
version: ?[]const u8 = null,
expectArgs: []const []const u8 = &[_][]const u8{},
options: []const OptionDescription,
};
pub fn findOption(comptime T: type, value: anytype, argsInfo: std.builtin.Type.Struct,
name: []const u8) ?type {
inline for (argsInfo.fields) |field| {
if (std.mem.eql(u8, field.name, name) and field.type == T) {
return @field(value, field.name);
}
}
return null;
}
pub fn printUsage(allocator: std.mem.Allocator, argsDescriptor: ArgDescriptor) void {
stdout.print("Usage: {s}{s}{s}\n", .{
argsDescriptor.name,
if (argsDescriptor.options.len > 0) " [OPTIONS]" else "",
if (argsDescriptor.expectArgs.len > 0) blk: {
const argsStr = std.mem.join(allocator, " ", argsDescriptor.expectArgs)
catch @panic("increase fixed buffer size");
break :blk std.fmt.allocPrint(allocator, " {s}", .{ argsStr })
catch @panic("increase fixed buffer size");
} else "",
}) catch unreachable;
}
pub fn printHelp(allocator: std.mem.Allocator, argsDescriptor: ArgDescriptor) void {
stdout.print("{s}{s} {s}\n", .{
argsDescriptor.name,
if (argsDescriptor.version) |version| " (" ++ version ++ ")" else "",
argsDescriptor.description orelse "",
}) catch unreachable;
stdout.print("\n", .{}) catch unreachable;
printUsage(allocator, argsDescriptor);
stdout.print("\nOptions:\n", .{}) catch unreachable;
inline for (argsDescriptor.options) |option| {
var buffer: [argsDescriptor.bufferSize]u8 = undefined;
const printed = std.fmt.bufPrint(&buffer, " {s}{s}{s}", .{
if (option.short) |short| "-" ++ short ++ "," else " ",
"--" ++ option.long,
if (option.arg) |arg| " " ++ arg.name else "",
}) catch @panic("increase fixed buffer size");
if (printed.len > 23) {
stdout.print("{s}\n {s}\n", .{ printed, option.help })
catch unreachable;
} else {
stdout.print("{s: <24} {s}\n", .{ printed, option.help }) catch unreachable;
}
}
}
pub const Args = struct {
const Self = @This();
switchMap: std.StringHashMap(bool),
optionMap: std.StringHashMap([]const u8),
arguments: std.ArrayList([]const u8),
pub fn getSwitch(self: Self, name: []const u8) bool {
return self.switchMap.get(name) orelse false;
}
pub fn getOption(self: Self, comptime T: type, name: []const u8) ?T {
return self.optionMap.get(name);
}
};
pub fn parser(argsDescriptor: ArgDescriptor) type {
return struct {
var buffer: [argsDescriptor.bufferSize]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();
var argsStore = Args{
.switchMap = std.StringHashMap(bool).init(allocator),
.optionMap = std.StringHashMap([]const u8).init(allocator),
.arguments = std.ArrayList([]const u8).init(allocator),
};
pub fn parse(args: [][:0]u8) Args {
if (argsDescriptor.withHelp) {
// Look for help and print it
for (args) |arg| {
if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) {
printHelp(allocator, argsDescriptor);
std.posix.exit(0);
}
}
var i: u16 = 1;
while (i < args.len) {
const arg = args[i];
if (arg[0] == '-') {
// Handle option in the block. i might be incremented additionally
// it the option expects an argument.
inline for (argsDescriptor.options) |option| {
if ((option.short != null and std.mem.eql(u8, arg[1..], option.short.?)) or
std.mem.eql(u8, arg[2..], option.long)) {
argsStore.switchMap.put(option.long, true)
catch @panic("increase fixed buffer size");
if (option.arg) |optionArg| {
_ = optionArg;
// We have an argument to the option
if (i > args.len - 1 or args[i + 1][0] == '-') {
// Missing argument
stderr.print("error: option {s} expected an argument\n", .{ arg })
catch unreachable;
printUsage(allocator, argsDescriptor);
}
argsStore.optionMap.put(option.long, args[i + 1])
catch @panic("increase fixed buffer size");
i += 1;
}
break;
}
} else {
// An option was provided but not described.
stderr.print("error: unknown option {s}\n", .{ arg }) catch unreachable;
printUsage(allocator, argsDescriptor);
std.posix.exit(1);
}
} else {
// Here are the argument to the program.
argsStore.arguments.append(args[i]) catch unreachable;
}
i += 1;
}
}
if (argsStore.arguments.items.len != argsDescriptor.expectArgs.len) {
stderr.print("error: incorrect number of arguments. Expected {} arguments, {} given.\n", .{
argsDescriptor.expectArgs.len, argsStore.arguments.items.len,
}) catch unreachable;
printUsage(allocator, argsDescriptor);
std.posix.exit(1);
}
return argsStore;
}
};
}
const std = @import("std");
const fontfile = @embedFile("dos_8x8_font_white.pbm");
pub fn DrawContext(comptime pwidth: u32, comptime pheight: u32) type {
return struct {
const Self = @This();
pub const contextWidth = pwidth;
pub const contextHeight = pheight;
pub var buffer = [_]u32 { 0 } ** (pwidth * pheight);
pub var color: u32 = 0xFFFFFFFF;
pub var thickness: u32 = 0;
var _transform: [6]f32 = .{ 1, 0, 0, 1, 0, 0 };
const _a = 0; const _b = 1; const _c = 2; const _d = 3; const _e = 4; const _f = 5;
var stack: [6]f32 = .{ 1, 0, 0, 1, 0, 0 };
// pub fn deinit(self: *Self, allocator: std.mem.Allocator) void {
// allocator.free(self.font);
// }
// resets (overrides) the current transformation to the identity matrix, and
// then invokes a transformation described by the arguments of this method.
// This lets you scale, rotate, translate (move), and skew the context.
// a c e
// The transformation matrix is described by: [ b d f ]
// 0 0 1
pub fn setTransform(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) void {
_transform = .{ a, b, c, d, e, f };
}
// multiplies the current transformation with the matrix described by the
// arguments of this method. This lets you scale, rotate, translate (move),
// and skew the context.
pub fn transform(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) void {
_transform[_a] = _transform[_a] * a + _transform[_c] * b;
_transform[_b] = _transform[_b] * a + _transform[_d] * b;
_transform[_c] = _transform[_a] * c + _transform[_c] * d;
_transform[_d] = _transform[_b] * c + _transform[_d] * d;
_transform[_e] = _transform[_a] * e + _transform[_c] * f + _transform[_e] * 1;
_transform[_f] = _transform[_b] * e + _transform[_d] * f + _transform[_f] * 1;
}
// retrieves the current transformation matrix being applied to the context.
pub fn getTransform() [6]f32 {
return _transform;
}
// Saves the entire state of the canvas by pushing the current state onto a
// stack.
// ⚠️ Only one level of stack for now.
pub fn save() !void {
stack = _transform;
}
// Restores the most recently saved canvas state by popping the top entry
// in the drawing state stack. If there is no saved state, this method does
// nothing.
pub fn restore() void {
_transform = stack;
}
// resets the rendering context to its default state, allowing it to be
// reused for drawing something else without having to explicitly reset all
// the properties.
pub fn reset() void {
_transform = .{ 1, 0, 0, 1, 0, 0 };
}
// adds a translation transformation to the current matrix.
pub fn translate(x: f32, y: f32) void {
_transform[_e] += x;
_transform[_f] += y;
}
// erases the pixels in a rectangular area by setting them to transparent
// black.
// ⚠️ Operates in buffer space (do not take the transformation matrix into account)
pub fn clearRect(x: i16, y: i16, width: i16, height: i16) void {
if (x == 0 and y == 0 and width == pwidth and height == pheight) {
@memset(&buffer, color);
} else {
@panic("clearReact on sizes different from the canvas is not yet implemented");
}
}
// Renders a rectangle with a starting point is at (x, y) and whose size is
// specified by width and height.
pub fn rect(x: i16, y: i16, width: i16, height: i16) void {
line(x, y, x + width, y);
line(x + width, y, x + width, y + height);
line(x + width, y + height, x, y + height);
line(x, y + height, x, y);
}
// Draws a line.
pub fn line(startx: i16, starty: i16, endx: i16, endy: i16) void {
// std.debug.assert(startx >= 0 and starty >= 0 and endx >= 0 and endy >= 0);
// This is commented because, although debug performances are much better,
// release performances are worst!
const builtin = @import("builtin");
if (builtin.mode == .Debug) {
// This whole block is an optimization for vertical and horizontal line
const ux =
_transform[_a] * @as(f32, @floatFromInt(startx)) +
_transform[_c] * @as(f32, @floatFromInt(starty)) + _transform[_e];
var uy =
_transform[_b] * @as(f32, @floatFromInt(startx)) +
_transform[_d] * @as(f32, @floatFromInt(starty)) + _transform[_f];
const vx =
_transform[_a] * @as(f32, @floatFromInt(endx)) +
_transform[_c] * @as(f32, @floatFromInt(endy)) + _transform[_e];
var vy =
_transform[_b] * @as(f32, @floatFromInt(endx)) +
_transform[_d] * @as(f32, @floatFromInt(endy)) + _transform[_f];
if (ux > 0 and uy > 0 and vx > 0 and vy > 0 and
ux < contextWidth and uy < contextHeight and vx < contextWidth and vy < contextHeight) {
// If a line is entirely in the canvas
if (ux == vx) {
// vertical line
const x: u16 = @intFromFloat(ux);
if (uy > vy) std.mem.swap(@TypeOf(uy), &uy, &vy);
var y: u16 = @intFromFloat(uy);
while (y < @as(u16, @intFromFloat(vy))) : (y += 1) {
buffer[y * contextWidth + x] = color;
}
return;
} else if (uy == vy) {
// horizontal line
var startBuffer = @as(u16, @intFromFloat(uy)) * contextWidth + @as(u16, @intFromFloat(ux));
var endBuffer = @as(u16, @intFromFloat(vy)) * contextWidth + @as(u16, @intFromFloat(vx));
if (startBuffer > endBuffer) std.mem.swap(@TypeOf(startBuffer), &startBuffer, &endBuffer);
@memset(buffer[startBuffer..endBuffer], color);
return;
}
}
}
// Otherwise, we use a general but slow algorithm
drawThickLine(startx, starty, endx, endy);
// drawLineOverlap(startx, starty, endx, endy, 0);
// drawLineWu(startx, starty, endx, endy, 0);
}
// Draws a point.
pub inline fn plot(x: i16, y: i16, acolor: u32) void {
// std.log.debug("plot x {} y {} width {} height {} index {} buffer.len {}", .{
// x, y, contextWidth, contextHeight,
// @as(u16, @bitCast(y)) * width + @as(u16, @bitCast(x)),
// buffer.len,
// });
const vx = _transform[_a] * @as(f32, @floatFromInt(x)) + _transform[_c] * @as(f32, @floatFromInt(y)) + _transform[_e];
const vy = _transform[_b] * @as(f32, @floatFromInt(x)) + _transform[_d] * @as(f32, @floatFromInt(y)) + _transform[_f];
if (vx >= 0 and vx < contextWidth and vy >= 0 and vy < contextHeight) {
buffer[@as(u16, @intFromFloat(vy)) * contextWidth + @as(u16, @intFromFloat(vx))] = acolor;
}
}
// Writes text at the specified position. x and y specifies the top left
// corner of the text box to be printed.
pub fn printText(x: i16, y: i16, text: []const u8) void {
// @setEvalBranchQuota(10000);
const fontparams = comptime lbl: {
// Check we deal with a P1 netpbm file (ASCII text)
std.debug.assert(fontfile[0] == 'P' and fontfile[1] == '1');
// Retrieve width and height
var i = 2;
while (std.ascii.isWhitespace(fontfile[i])) i += 1;
var j = i;
while (!std.ascii.isWhitespace(fontfile[j])) j += 1;
const fontwidth = try std.fmt.parseInt(usize, fontfile[i..j], 10);
i = j;
while (std.ascii.isWhitespace(fontfile[i])) i += 1;
j = i;
while (!std.ascii.isWhitespace(fontfile[j])) j += 1;
const fontheight = try std.fmt.parseInt(usize, fontfile[i..j], 10);
// Get position of first value
while (std.ascii.isWhitespace(fontfile[j])) j += 1;
break :lbl .{ fontwidth, fontheight, j };
};
const fontwidth = fontparams[0];
const fontindex = fontparams[2];
for (text, 0..) |c, cindex| {
const cusize = @as(usize, @intCast(c));
for (0..8) |j| {
for (0..8) |i| {
// fontwidth + 1 because of the \n
if (fontfile[fontindex + j * (fontwidth + 1) + cusize * 8 + i] != '0') {
plot(x + @as(i16, @intCast(i)) + @as(i16, @intCast(cindex * 8)), y + @as(i16, @intCast(j)), color);
}
}
}
}
}
// Modified Bresenham draw(line) with optional overlap. Required for drawThickLine().
// Overlap draws additional pixel when changing minor direction. For standard bresenham overlap, choose LINE_OVERLAP_NONE (0).
//
// Sample line:
//
// 00+
// -0000+
// -0000+
// -00
//
// 0 pixels are drawn for normal line without any overlap LINE_OVERLAP_NONE
// + pixels are drawn if LINE_OVERLAP_MAJOR
// - pixels are drawn if LINE_OVERLAP_MINOR
const Overlap = enum(u8) {
LINE_OVERLAP_NONE = 0,
LINE_OVERLAP_MINOR = 1,
LINE_OVERLAP_MAJOR = 2,
LINE_OVERLAP_BOTH = 3,
};
fn drawLineOverlap(pstartx: i16, pstarty: i16, endx: i16, endy: i16, aOverlap: u8) void {
var tStepX: i16 = 0;
var tStepY: i16 = 0;
var tDeltaXTimes2: i16 = 0;
var tDeltaYTimes2: i16 = 0;
var tError: i16 = 0;
var startx = pstartx;
var starty = pstarty;
// calculate direction
var tDeltaX = endx - startx;
var tDeltaY = endy - starty;
if (tDeltaX < 0) {
tDeltaX = -tDeltaX;
tStepX = -1;
} else {
tStepX = 1;
}
if (tDeltaY < 0) {
tDeltaY = -tDeltaY;
tStepY = -1;
} else {
tStepY = 1;
}
tDeltaXTimes2 = tDeltaX << 1;
tDeltaYTimes2 = tDeltaY << 1;
// draw start pixel
plot(startx, starty, color);
if (tDeltaX > tDeltaY) {
// start value represents a half step in Y direction
tError = tDeltaYTimes2 - tDeltaX;
while (startx != endx) {
// step in main direction
startx += tStepX;
if (tError >= 0) {
if (aOverlap & @intFromEnum(Overlap.LINE_OVERLAP_MAJOR) != 0) {
// draw pixel in main direction before changing
plot(startx, starty, color);
}
// change Y
starty += tStepY;
if (aOverlap & @intFromEnum(Overlap.LINE_OVERLAP_MINOR) != 0) {
// draw pixel in minor direction before changing
plot(startx - tStepX, starty, color);
}
tError -= tDeltaXTimes2;
}
tError += tDeltaYTimes2;
plot(startx, starty, color);
}
} else {
tError = tDeltaXTimes2 - tDeltaY;
while (starty != endy) {
starty += tStepY;
if (tError >= 0) {
if (aOverlap & @intFromEnum(Overlap.LINE_OVERLAP_MAJOR) != 0) {
// draw pixel in main direction before changing
plot(startx, starty, color);
}
startx += tStepX;
if (aOverlap & @intFromEnum(Overlap.LINE_OVERLAP_MINOR) != 0) {
// draw pixel in minor direction before changing
plot(startx, starty - tStepY, color);
}
tError -= tDeltaYTimes2;
}
tError += tDeltaXTimes2;
plot(startx, starty, color);
}
}
}
// fractional part of x
fn fpart(x: f32) f32 {
return x - @floor(x);
}
fn rfpart(x: f32) f32 {
return 1 - fpart(x);
}
fn shadeColor(R: f32, G: f32, B: f32, ratio: f32) u32 {
return @as(u32, @intFromFloat(R * ratio)) |
@as(u32, @intFromFloat(G * ratio)) << 8 |
@as(u32, @intFromFloat(B * ratio)) << 16 |
0xFF000000; // Always full alpha
}
fn drawLineWu(px0: i16, py0: i16, px1: i16, py1: i16, unused: u8) void {
std.log.debug("drawLineWu {} {} {} {}", .{ px0, py0, px1, py1 });
_ = unused;
var x0: f32 = @floatFromInt(px0);
var y0: f32 = @floatFromInt(py0);
var x1: f32 = @floatFromInt(px1);
var y1: f32 = @floatFromInt(py1);
const R: f32 = @floatFromInt(color & 0x000000FF);
const G: f32 = @floatFromInt((color & 0x0000FF00) >> 8);
const B: f32 = @floatFromInt((color & 0x00FF0000) >> 16);
const steep = @abs(y1 - y0) > @abs(x1 - x0);
if (steep) {
std.mem.swap(f32, &x0, &y0);
std.mem.swap(f32, &x1, &y1);
std.log.debug("drawLineWu2 {d} {d} {d} {d}", .{ x0, y0, x1, y1 });
}
if (x0 > x1) {
std.mem.swap(f32, &x0, &x1);
std.mem.swap(f32, &y0, &y1);
std.log.debug("drawLineWu3 {d} {d} {d} {d}", .{ x0, y0, x1, y1 });
}
const dx = x1 - x0;
const dy = y1 - y0;
var gradient: f32 = 1.0;
if (dx != 0.0) {
gradient = dy / dx;
}
// handle first endpoint
var xend = @round(x0);
var yend = y0 + gradient * (xend - x0);
var xgap = rfpart(x0 + 0.5);
const xpxl1: i16 = @intFromFloat(xend); // this will be used in the main loop
const ypxl1: i16 = @intFromFloat(@floor(yend));
if (steep) {
plot(ypxl1 , xpxl1 , shadeColor(R, G, B, rfpart(yend) * xgap));
plot(ypxl1 + 1, xpxl1 , shadeColor(R, G, B, fpart(yend) * xgap));
} else {
plot(xpxl1 , ypxl1 , shadeColor(R, G, B, rfpart(yend) * xgap));
plot(xpxl1 , ypxl1 + 1, shadeColor(R, G, B, fpart(yend) * xgap));
}
var intery = yend + gradient; // first y-intersection for the main loop
// handle second endpoint
std.log.debug("x1 {d} y1 {d}", .{ x1, y1 });
xend = @round(x1);
yend = y1 + gradient * (xend - x1);
std.log.debug("xend {d} yend {d}", .{ xend, yend });
xgap = fpart(x1 + 0.5);
const xpxl2: i16 = @intFromFloat(xend); //this will be used in the main loop
const ypxl2: i16 = @intFromFloat(@floor(yend));
std.log.debug("xpxl2 {} ypxl2 {}", .{ xpxl2, ypxl2 });
if (steep) {
plot(ypxl2 , xpxl2 , shadeColor(R, G, B, rfpart(yend) * xgap));
plot(ypxl2 + 1, xpxl2 , shadeColor(R, G, B, fpart(yend) * xgap));
} else {
plot(xpxl2 , ypxl2 , shadeColor(R, G, B, rfpart(yend) * xgap));
plot(xpxl2 , ypxl2 + 1, shadeColor(R, G, B, fpart(yend) * xgap));
}
// main loop
if (steep) {
var i = xpxl1 + 1;
while (i < xpxl2 - 1) {
plot(@as(i16, @intFromFloat(@floor(intery))) , i, shadeColor(R, G, B, rfpart(intery)));
plot(@as(i16, @intFromFloat(@floor(intery))) + 1, i, shadeColor(R, G, B, fpart(intery)));
intery = intery + gradient;
i += 1;
}
} else {
var i = xpxl1 + 1;
while (i < xpxl2 - 1) {
plot(i, @as(i16, @intFromFloat(@floor(intery))) , shadeColor(R, G, B, rfpart(intery)));
plot(i, @as(i16, @intFromFloat(@floor(intery))) + 1, shadeColor(R, G, B, fpart(intery)));
intery = intery + gradient;
i += 1;
}
}
}
//
// The same as before, but no clipping to display range, some pixel are drawn twice (because of using LINE_OVERLAP_BOTH)
// and direction of thickness changes for each octant (except for LINE_THICKNESS_MIDDLE and thickness value is odd)
// thicknessMode can be LINE_THICKNESS_MIDDLE or any other value
//
fn drawThickLine(pstartx: i16, pstarty: i16, pendx: i16, pendy: i16) void {
var tStepX: i16 = 0;
var tStepY: i16 = 0;
var tDeltaXTimes2: i16 = 0;
var tDeltaYTimes2: i16 = 0;
var tError: i16 = 0;
var startx = pstartx;
var starty = pstarty;
var endx = pendx;
var endy = pendy;
var tDeltaY = startx - endx;
var tDeltaX = endy - startx;
// mirror 4 quadrants to one and adjust deltas and stepping direction
if (tDeltaX < 0) {
tDeltaX = -tDeltaX;
tStepX = -1;
} else {
tStepX = 1;
}
if (tDeltaY < 0) {
tDeltaY = -tDeltaY;
tStepY = -1;
} else {
tStepY = 1;
}
tDeltaXTimes2 = tDeltaX << 1;
tDeltaYTimes2 = tDeltaY << 1;
var tOverlap: Overlap = Overlap.LINE_OVERLAP_NONE;
// which octant are we now
if (tDeltaX > tDeltaY) {
// if (we want to draw the original coordinate in the middle of the thick line)
{
// adjust draw start point
tError = tDeltaYTimes2 - tDeltaX;
var i = thickness / 2;
while (i > 0) {
// change X (main direction here)
startx -= tStepX;
endx -= tStepX;
if (tError >= 0) {
// change Y
starty -= tStepY;
endy -= tStepY;
tError -= tDeltaXTimes2;
}
tError += tDeltaYTimes2;
i -= 1;
}
}
drawLineOverlap(startx, starty, endx, endy, @intFromEnum(tOverlap));
// drawLineWu(startx, starty, endx, endy, @intFromEnum(tOverlap));
// draw thickness lines
tError = tDeltaYTimes2 - tDeltaX;
var i = thickness;
while (i > 1) {
// change X (main direction here)
startx += tStepX;
endx += tStepX;
tOverlap = Overlap.LINE_OVERLAP_NONE;
if (tError >= 0) {
// change Y
startx += tStepY;
endy += tStepY;
tError -= tDeltaXTimes2;
tOverlap = Overlap.LINE_OVERLAP_BOTH;
}
tError += tDeltaYTimes2;
drawLineOverlap(startx, starty, endx, endy, @intFromEnum(tOverlap));
// drawLineWu(startx, starty, endx, endy, @intFromEnum(tOverlap));
i -= 1;
}
} else {
// if (we want to draw the original coordinate in the middle of the thick line)
{
tError = tDeltaXTimes2 - tDeltaY;
var i = thickness / 2;
while (i > 0) {
starty -= tStepY;
endy -= tStepY;
if (tError >= 0) {
startx -= tStepX;
endx -= tStepX;
tError -= tDeltaYTimes2;
}
tError += tDeltaXTimes2;
i -= 1;
}
}
drawLineOverlap(startx, starty, endx, endy, @intFromEnum(tOverlap));
// drawLineWu(startx, starty, endx, endy, @intFromEnum(tOverlap));
tError = tDeltaXTimes2 - tDeltaY;
var i = thickness;
while (i > 1) {
starty += tStepY;
endy += tStepY;
tOverlap = Overlap.LINE_OVERLAP_NONE;
if (tError >= 0) {
startx += tStepX;
endx += tStepX;
tError -= tDeltaYTimes2;
tOverlap = Overlap.LINE_OVERLAP_BOTH;
}
tError += tDeltaXTimes2;
drawLineOverlap(startx, starty, endx, endy, @intFromEnum(tOverlap));
// drawLineWu(startx, starty, endx, endy, @intFromEnum(tOverlap));
i -= 1;
}
}
}
};
}
// https://zig.news/david_vanderson/interfaces-in-zig-o1c
const std = @import("std");
pub const sdl = @cImport({
@cInclude("SDL2/SDL.h");
});
pub const MouseMove = struct {
x: i32,
y: i32,
dx: i32,
dy: i32,
};
pub const MouseButton = enum(u8) {
Left = 1,
Center = 2,
Right = 3,
};
pub const MouseButtonEvent = struct {
button: MouseButton,
};
pub const MouseWheel = struct {
y: i32,
};
// From SDL_Scancode
// These values are from usage page 0x07 (USB keyboard page).
pub const Scancode = enum(u32) {
A = 4,
B = 5,
C = 6,
D = 7,
E = 8,
F = 9,
G = 10,
H = 11,
I = 12,
J = 13,
K = 14,
L = 15,
M = 16,
N = 17,
O = 18,
P = 19,
Q = 20,
R = 21,
S = 22,
T = 23,
U = 24,
V = 25,
W = 26,
X = 27,
Y = 28,
Z = 29,
K1 = 30,
K2 = 31,
K3 = 32,
K4 = 33,
K5 = 34,
K6 = 35,
K7 = 36,
K8 = 37,
K9 = 38,
K0 = 39,
RETURN = 40,
ESCAPE = 41,
BACKSPACE = 42,
TAB = 43,
SPACE = 44,
MINUS = 45,
EQUALS = 46,
LEFTBRACKET = 47,
RIGHTBRACKET = 48,
BACKSLASH = 49, // < Located at the lower left of the return
// key on ISO keyboards and at the right end
// of the QWERTY row on ANSI keyboards.
// Produces REVERSE SOLIDUS (backslash) and
// VERTICAL LINE in a US layout, REVERSE
// SOLIDUS and VERTICAL LINE in a UK Mac
// layout, NUMBER SIGN and TILDE in a UK
// Windows layout, DOLLAR SIGN and POUND SIGN
// in a Swiss German layout, NUMBER SIGN and
// APOSTROPHE in a German layout, GRAVE
// ACCENT and POUND SIGN in a French Mac
// layout, and ASTERISK and MICRO SIGN in a
// French Windows layout.
//
NONUSHASH = 50, // < ISO USB keyboards actually use this code
// instead of 49 for the same key, but all
// OSes I've seen treat the two codes
// identically. So, as an implementor, unless
// your keyboard generates both of those
// codes and your OS treats them differently,
// you should generate BACKSLASH
// instead of this code. As a user, you
// should not rely on this code because SDL
// will never generate it with most (all?)
// keyboards.
//
SEMICOLON = 51,
APOSTROPHE = 52,
GRAVE = 53, // < Located in the top left corner (on both ANSI
// and ISO keyboards). Produces GRAVE ACCENT and
// TILDE in a US Windows layout and in US and UK
// Mac layouts on ANSI keyboards, GRAVE ACCENT
// and NOT SIGN in a UK Windows layout, SECTION
// SIGN and PLUS-MINUS SIGN in US and UK Mac
// layouts on ISO keyboards, SECTION SIGN and
// DEGREE SIGN in a Swiss German layout (Mac:
// only on ISO keyboards), CIRCUMFLEX ACCENT and
// DEGREE SIGN in a German layout (Mac: only on
// ISO keyboards), SUPERSCRIPT TWO and TILDE in a
// French Windows layout, COMMERCIAL AT and
// NUMBER SIGN in a French Mac layout on ISO
// keyboards, and LESS-THAN SIGN and GREATER-THAN
// SIGN in a Swiss German, German, or French Mac
// layout on ANSI keyboards.
//
COMMA = 54,
PERIOD = 55,
SLASH = 56,
CAPSLOCK = 57,
F1 = 58,
F2 = 59,
F3 = 60,
F4 = 61,
F5 = 62,
F6 = 63,
F7 = 64,
F8 = 65,
F9 = 66,
F10 = 67,
F11 = 68,
F12 = 69,
PRINTSCREEN = 70,
SCROLLLOCK = 71,
PAUSE = 72,
INSERT = 73, // < insert on PC, help on some Mac keyboards (but
// does send code 73, not 117)
HOME = 74,
PAGEUP = 75,
DELETE = 76,
END = 77,
PAGEDOWN = 78,
RIGHT = 79,
LEFT = 80,
DOWN = 81,
UP = 82,
NUMLOCKCLEAR = 83, // < num lock on PC, clear on Mac keyboards
KP_DIVIDE = 84,
KP_MULTIPLY = 85,
KP_MINUS = 86,
KP_PLUS = 87,
KP_ENTER = 88,
KP_1 = 89,
KP_2 = 90,
KP_3 = 91,
KP_4 = 92,
KP_5 = 93,
KP_6 = 94,
KP_7 = 95,
KP_8 = 96,
KP_9 = 97,
KP_0 = 98,
KP_PERIOD = 99,
NONUSBACKSLASH = 100, // < This is the additional key that ISO
// keyboards have over ANSI ones,
// located between left shift and Y.
// Produces GRAVE ACCENT and TILDE in a
// US or UK Mac layout, REVERSE SOLIDUS
// (backslash) and VERTICAL LINE in a
// US or UK Windows layout, and
// LESS-THAN SIGN and GREATER-THAN SIGN
// in a Swiss German, German, or French
// layout.
APPLICATION = 101, // < windows contextual menu, compose
POWER = 102, // < The USB document says this is a status flag,
// not a physical key - but some Mac keyboards
// do have a power key.
KP_EQUALS = 103,
F13 = 104,
F14 = 105,
F15 = 106,
F16 = 107,
F17 = 108,
F18 = 109,
F19 = 110,
F20 = 111,
F21 = 112,
F22 = 113,
F23 = 114,
F24 = 115,
EXECUTE = 116,
HELP = 117, // < AL Integrated Help Center
MENU = 118, // < Menu (show menu)
SELECT = 119,
STOP = 120, // < AC Stop
AGAIN = 121, // < AC Redo/Repeat
UNDO = 122, // < AC Undo
CUT = 123, // < AC Cut
COPY = 124, // < AC Copy
PASTE = 125, // < AC Paste
FIND = 126, // < AC Find
MUTE = 127,
VOLUMEUP = 128,
VOLUMEDOWN = 129,
// not sure whether there's a reason to enable these
// LOCKINGCAPSLOCK = 130,
// LOCKINGNUMLOCK = 131,
// LOCKINGSCROLLLOCK = 132,
KP_COMMA = 133,
KP_EQUALSAS400 = 134,
INTERNATIONAL1 = 135, // < used on Asian keyboards, see
// footnotes in USB doc
INTERNATIONAL2 = 136,
INTERNATIONAL3 = 137, // < Yen
INTERNATIONAL4 = 138,
INTERNATIONAL5 = 139,
INTERNATIONAL6 = 140,
INTERNATIONAL7 = 141,
INTERNATIONAL8 = 142,
INTERNATIONAL9 = 143,
LANG1 = 144, // < Hangul/English toggle
LANG2 = 145, // < Hanja conversion
LANG3 = 146, // < Katakana
LANG4 = 147, // < Hiragana
LANG5 = 148, // < Zenkaku/Hankaku
LANG6 = 149, // < reserved
LANG7 = 150, // < reserved
LANG8 = 151, // < reserved
LANG9 = 152, // < reserved
ALTERASE = 153, // < Erase-Eaze
SYSREQ = 154,
CANCEL = 155, // < AC Cancel
CLEAR = 156,
PRIOR = 157,
RETURN2 = 158,
SEPARATOR = 159,
OUT = 160,
OPER = 161,
CLEARAGAIN = 162,
CRSEL = 163,
EXSEL = 164,
KP_00 = 176,
KP_000 = 177,
THOUSANDSSEPARATOR = 178,
DECIMALSEPARATOR = 179,
CURRENCYUNIT = 180,
CURRENCYSUBUNIT = 181,
KP_LEFTPAREN = 182,
KP_RIGHTPAREN = 183,
KP_LEFTBRACE = 184,
KP_RIGHTBRACE = 185,
KP_TAB = 186,
KP_BACKSPACE = 187,
KP_A = 188,
KP_B = 189,
KP_C = 190,
KP_D = 191,
KP_E = 192,
KP_F = 193,
KP_XOR = 194,
KP_POWER = 195,
KP_PERCENT = 196,
KP_LESS = 197,
KP_GREATER = 198,
KP_AMPERSAND = 199,
KP_DBLAMPERSAND = 200,
KP_VERTICALBAR = 201,
KP_DBLVERTICALBAR = 202,
KP_COLON = 203,
KP_HASH = 204,
KP_SPACE = 205,
KP_AT = 206,
KP_EXCLAM = 207,
KP_MEMSTORE = 208,
KP_MEMRECALL = 209,
KP_MEMCLEAR = 210,
KP_MEMADD = 211,
KP_MEMSUBTRACT = 212,
KP_MEMMULTIPLY = 213,
KP_MEMDIVIDE = 214,
KP_PLUSMINUS = 215,
KP_CLEAR = 216,
KP_CLEARENTRY = 217,
KP_BINARY = 218,
KP_OCTAL = 219,
KP_DECIMAL = 220,
KP_HEXADECIMAL = 221,
LCTRL = 224,
LSHIFT = 225,
LALT = 226, // < alt, option
LGUI = 227, // < windows, command (apple), meta
RCTRL = 228,
RSHIFT = 229,
RALT = 230, // < alt gr, option
RGUI = 231, // < windows, command (apple), meta
MODE = 257, // < I'm not sure if this is really not covered
// by any of the above, but since there's a
// special KMOD_MODE for it I'm adding it here
//
};
pub const Keycode = enum(u8) {
None,
};
pub const Mod = enum(u16) {
LSHIFT = 1,
RSHIFT = 2,
LCTRL = 64,
};
pub const KeyEvent = struct {
mod: u16,
scancode: Scancode,
keycode: Keycode,
};
pub const EventType = enum {
MouseMove,
MouseClick,
MouseDown,
MouseUp,
MouseWheel,
KeyDown,
};
pub const InputEvent = union(EventType) {
MouseMove: MouseMove,
MouseClick: MouseButtonEvent,
MouseDown: MouseButtonEvent,
MouseUp: MouseButtonEvent,
MouseWheel: MouseWheel,
KeyDown: KeyEvent,
};
pub const IOAdapter = struct {
getEventFn: *const fn (*IOAdapter) ?InputEvent,
drawImageFn: *const fn (*IOAdapter, []const u32, u16, u16, u16, u16) void,
renderSceneFn: *const fn (*IOAdapter) void,
pub fn getEvent(adapter: *IOAdapter) ?InputEvent {
return adapter.getEventFn(adapter);
}
pub fn drawImage(adapter: *IOAdapter, image: []const u32, sx: u16, sy: u16, sWidth: u16, sHeight: u16) void {
return adapter.drawImageFn(adapter, image, sx, sy, sWidth, sHeight);
}
pub fn renderScene(adapter: *IOAdapter) void {
return adapter.renderSceneFn(adapter);
}
};
pub const SDLAdapter = struct {
const Self = @This();
window: *sdl.SDL_Window,
texture: *sdl.SDL_Texture,
renderer: *sdl.SDL_Renderer,
width: usize,
height: usize,
// tv: std.posix.timeval,
interface: IOAdapter,
pub fn init(width: u16, height: u16) anyerror!Self {
if (sdl.SDL_Init(sdl.SDL_INIT_VIDEO) != 0) {
sdl.SDL_Log("Unable to initialize SDL: %s", sdl.SDL_GetError());
return error.SDLInitializationFailed;
}
errdefer sdl.SDL_Quit();
const window = sdl.SDL_CreateWindow("level", sdl.SDL_WINDOWPOS_UNDEFINED,
sdl.SDL_WINDOWPOS_UNDEFINED, width, height, sdl.SDL_WINDOW_OPENGL) orelse {
sdl.SDL_Log("Unable to create window: %s", sdl.SDL_GetError());
return error.SDLInitializationFailed;
};
errdefer sdl.SDL_DestroyWindow(window);
const renderer = sdl.SDL_CreateRenderer(window, -1, 0) orelse {
sdl.SDL_Log("Unable to create renderer: %s", sdl.SDL_GetError());
return error.SDLInitializationFailed;
};
errdefer sdl.SDL_DestroyRenderer(renderer);
const texture = sdl.SDL_CreateTexture(renderer, sdl.SDL_PIXELFORMAT_RGBA32,
sdl.SDL_TEXTUREACCESS_STREAMING, width, height) orelse {
sdl.SDL_Log("Unable to create texture: %s", sdl.SDL_GetError());
return error.SDLInitializationFailed;
};
errdefer sdl.SDL_DestroyTexture(texture);
return Self{
.window = window,
.texture = texture,
.renderer = renderer,
.width = width,
.height = height,
.interface = IOAdapter {
.getEventFn = getEvent,
.drawImageFn = drawImage,
.renderSceneFn = renderScene,
},
};
}
pub fn deinit(self: *Self) void {
sdl.SDL_DestroyRenderer(self.renderer);
sdl.SDL_DestroyTexture(self.texture);
sdl.SDL_DestroyWindow(self.window);
sdl.SDL_Quit();
}
pub fn getEvent(adapter: *IOAdapter) ?InputEvent {
const self: *SDLAdapter = @fieldParentPtr("interface", adapter);
_ = self;
var event: sdl.SDL_Event = undefined;
if (sdl.SDL_PollEvent(&event) != 0) {
switch (event.type) {
sdl.SDL_KEYDOWN => {
return InputEvent{
.KeyDown = KeyEvent{
.mod = event.key.keysym.mod,
.scancode = @enumFromInt(event.key.keysym.scancode),
.keycode = Keycode.None,
},
};
},
sdl.SDL_MOUSEMOTION => {
return InputEvent{
.MouseMove = MouseMove{
.x = event.motion.x,
.y = event.motion.y,
.dx = event.motion.xrel,
.dy = event.motion.yrel,
},
};
},
sdl.SDL_MOUSEBUTTONDOWN => {
return InputEvent{ .MouseDown = MouseButtonEvent{ .button = @enumFromInt(event.button.button) } };
},
sdl.SDL_MOUSEBUTTONUP => {
return InputEvent{ .MouseUp = MouseButtonEvent{ .button = @enumFromInt(event.button.button) } };
},
sdl.SDL_MOUSEWHEEL => {
return InputEvent{ .MouseWheel = MouseWheel{ .y = event.wheel.y } };
},
else => {},
}
}
return null;
}
pub fn drawImage(adapter: *IOAdapter, image: []const u32, sx: u16, sy: u16, sWidth: u16, sHeight: u16) void {
const self: *SDLAdapter = @fieldParentPtr("interface", adapter);
const texture = self.texture;
var buffer: [*c]u32 = undefined;
var pitch: i32 = undefined;
const res = sdl.SDL_LockTexture(texture, null, @as([*c]?*anyopaque, @ptrCast(&buffer)), &pitch);
if (res < 0) {
sdl.SDL_Log("Unable to lock texture: %s", sdl.SDL_GetError());
std.posix.exit(0);
}
if (sx == 0 and sy == 0 and sWidth == self.width and sHeight == self.height) {
std.mem.copyForwards(u32, buffer[0..image.len], image);
} else if (sWidth * 2 == self.width and sHeight * 2 == self.height) {
@setRuntimeSafety(false); // Too slow otherwise
for (0..self.height) |j| {
const offset = j * self.width;
const sourceOffset = j / 2 * sWidth;
for (0..self.width) |i| {
const imageIndex: usize = sourceOffset + i / 2;
buffer[offset + i] = image[imageIndex];
}
}
@setRuntimeSafety(true);
} else {
@setRuntimeSafety(false); // Too slow otherwise
const ifactor: f32 = @as(f32, @floatFromInt(sWidth)) / @as(f32, @floatFromInt(self.width));
const jfactor: f32 = @as(f32, @floatFromInt(sHeight)) / @as(f32, @floatFromInt(self.height));
for (0..self.height) |j| {
const offset = j * self.width;
const sourceOffset = @as(usize, @intFromFloat(@as(f32, @floatFromInt(j)) * jfactor)) * sWidth;
for (0..self.width) |i| {
const imageIndex: usize = sourceOffset + @as(usize, @intFromFloat(@as(f32, @floatFromInt(i)) * ifactor));
buffer[offset + i] = image[imageIndex];
}
}
@setRuntimeSafety(true);
}
sdl.SDL_UnlockTexture(texture);
}
pub fn renderScene(adapter: *IOAdapter) void {
const self: *SDLAdapter = @fieldParentPtr("interface", adapter);
if (sdl.SDL_SetRenderDrawColor(self.renderer, 0x00, 0x00, 0x00, 0xFF) < 0) {
sdl.SDL_Log("Unable to draw color: %s", sdl.SDL_GetError());
std.posix.exit(0);
}
if (sdl.SDL_RenderClear(self.renderer) < 0) {
sdl.SDL_Log("Unable to clear: %s", sdl.SDL_GetError());
std.posix.exit(0);
}
// blit the surface
if (sdl.SDL_RenderCopy(self.renderer, self.texture, null, null) < 0) {
sdl.SDL_Log("Unable to copy texture: %s", sdl.SDL_GetError());
std.posix.exit(0);
}
sdl.SDL_RenderPresent(self.renderer);
}
};
const std = @import("std");
// Load a file into a buffer using mmap
pub fn load(pathname: []const u8) ![]align(4096) const u8 {
var file = try std.fs.cwd().openFile(pathname, .{});
defer file.close();
const size = try file.getEndPos();
const buffer = try std.posix.mmap(
null,
size,
std.posix.PROT.READ,
.{ .TYPE = .SHARED },
file.handle,
0,
);
errdefer std.posix.munmap(buffer);
return buffer;
}
const Ptype = enum {
P1, P2, P3, P4, P5, P6,
};
pub fn writePgm(comptime P: Ptype, width: usize, height: usize, pixels: []const u8, filepath: []const u8) !void {
if (width * height != pixels.len) {
return error.IncorrectSize;
}
// Create file
const file = try std.fs.cwd().createFile(
filepath,
.{ .read = true },
);
defer file.close();
switch (P) {
.P1 => {
// Prepare PGM header (https://en.wikipedia.org/wiki/Netpbm)
var buffer: [255]u8 = [_]u8{ 0 } ** 255;
const pgmHeader = try std.fmt.bufPrint(&buffer, "P1\n{} {}\n", .{ width, height });
// Write to file
try file.writeAll(pgmHeader);
for (0..height) |j| {
for (0..width) |i| {
if (pixels[j * width + i] != 0) {
try file.writeAll("1 ");
} else {
try file.writeAll("0 ");
}
}
try file.writeAll("\n");
}
},
.P6 => {
// Prepare PGM header (https://en.wikipedia.org/wiki/Netpbm)
var buffer: [255]u8 = [_]u8{ 0 } ** 255;
const pgmHeader = try std.fmt.bufPrint(&buffer, "P6\n{} {}\n255\n", .{ width, height });
// Write to file
try file.writeAll(pgmHeader);
const stdout = std.io.getStdOut().writer();
for (pixels) |p| { try stdout.print("{} ", .{ p }); }
try file.writeAll(pixels);
},
else => return error.NotImplemented,
}
}
pub fn writePam32(width: usize, height: usize, pixels: []const u8, filepath: []const u8) !void {
// Create file
const file = try std.fs.cwd().createFile(
filepath,
.{ .read = true },
);
defer file.close();
// Prepare PGM header (https://en.wikipedia.org/wiki/Netpbm)
var buffer: [255]u8 = [_]u8{ 0 } ** 255;
const pgmHeader = try std.fmt.bufPrint(&buffer, "P7\nWIDTH {}\nHEIGHT {}\nDEPTH 4\nMAXVAL 255\nTUPLTYPE RGB_ALPHA\nENDHDR\n", .{ width, height });
// Write to file
try file.writeAll(pgmHeader);
try file.writeAll(pixels);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment