commit de3182744be1a44fd5991fb198f1472d2bdc280c Author: copygirl Date: Sat Apr 20 14:51:19 2024 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d864d9e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/zig-cache/ +/zig-out/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..4dbc419 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [{ + "name": "Debug", + "type": "lldb", + "preLaunchTask": "build", + "request": "launch", + "cwd": "${workspaceFolder}", + "program": "${workspaceFolder}/zig-out/bin/gridstep", + "args": [], + }] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..7baa495 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,16 @@ +{ + "version": "2.0.0", + "tasks": [{ + "label": "build", + "group": { "kind": "build", "isDefault": true }, + "type": "shell", + "command": "zig build", + "problemMatcher": [] + },{ + "label": "test", + "group": { "kind": "test", "isDefault": true }, + "type": "shell", + "command": "zig build test", + "problemMatcher": [] + }] +} diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..b58fd3a --- /dev/null +++ b/build.zig @@ -0,0 +1,38 @@ +const std = @import("std"); +const sdl = @import("sdl"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const sdl_sdk = sdl.init(b, null); + + const exe = b.addExecutable(.{ + .name = "gridstep", + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + sdl_sdk.link(exe, .dynamic); + exe.linkSystemLibrary("sdl2_image"); + exe.linkSystemLibrary("libpng"); + + exe.root_module.addImport("sdl", sdl_sdk.getNativeModule()); + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| run_cmd.addArgs(args); + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + const run_unit_tests = b.addRunArtifact(unit_tests); + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..eed2f0c --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,18 @@ +.{ + .name = "gridstep", + .version = "0.0.0", + .minimum_zig_version = "0.11.0", + + .dependencies = .{ + .sdl = .{ + .url = "https://github.com/MasterQ32/SDL.zig/archive/39fb8355cccb45a241a891c4848ab925af20fee4.tar.gz", + .hash = "12203537fc1357c4efce1c7a5ae3b407b9070490d83acd69aa3363b6b8e8a2b27870", + }, + }, + + .paths = .{ + "src", + "build.zig", + "build.zig.zon", + }, +} diff --git a/gfx/sprite_map.png b/gfx/sprite_map.png new file mode 100644 index 0000000..85aba4f Binary files /dev/null and b/gfx/sprite_map.png differ diff --git a/gfx/sprite_map.xcf b/gfx/sprite_map.xcf new file mode 100644 index 0000000..e265c91 Binary files /dev/null and b/gfx/sprite_map.xcf differ diff --git a/src/canvas.zig b/src/canvas.zig new file mode 100644 index 0000000..c25715d --- /dev/null +++ b/src/canvas.zig @@ -0,0 +1,102 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const sdl = @import("sdl"); +const Window = @import("./window.zig"); +const makeSdlError = Window.makeSdlError; + +const Canvas = @This(); + +allocator: Allocator, +surface: *sdl.SDL_Surface, +pixels: []Pixel, +width: usize, +height: usize, + +pub fn init(allocator: Allocator, width: usize, height: usize) !*Canvas { + const surface = sdl.SDL_CreateRGBSurfaceWithFormat( + 0, + @intCast(width), + @intCast(height), + 0, + sdl.SDL_PIXELFORMAT_ABGR8888, + ) orelse + return makeSdlError(); + + const pixels_ptr: [*]Pixel = @alignCast(@ptrCast(surface.pixels)); + const pixels_len = width * height; + const pixels = pixels_ptr[0..pixels_len]; + + const result = try allocator.create(Canvas); + result.* = .{ + .allocator = allocator, + .surface = surface, + .pixels = pixels, + .width = width, + .height = height, + }; + return result; +} + +pub fn deinit(self: *Canvas) void { + sdl.SDL_FreeSurface(self.surface); + self.allocator.destroy(self); +} + +pub fn getUnsafe(self: *Canvas, x: usize, y: usize) *Pixel { + const index = x + y * self.width; + return &self.pixels[index]; +} + +pub fn get(self: *Canvas, x: usize, y: usize) *Pixel { + std.debug.assert(x < self.width); + std.debug.assert(y < self.height); + return self.getUnsafe(x, y); +} + +pub fn put(self: *Canvas, x: usize, y: usize, value: Pixel) void { + self.get(x, y).* = value; +} + +pub fn putRect(self: *Canvas, x: usize, y: usize, w: usize, h: usize, value: Pixel) void { + std.debug.assert(x + w <= self.width); + std.debug.assert(y + h <= self.height); + const start_index = x + y * self.width; + for (0..h) |yo| { + const row_index = start_index + yo * self.width; + @memset(self.pixels[row_index..][0..w], value); + } +} + +pub fn putSprite(self: *Canvas, x: usize, y: usize, sprite: *const Sprite) void { + std.debug.assert(x + 16 <= self.width); + std.debug.assert(y + 16 <= self.height); + for (0..16) |yo| + for (0..16) |xo| { + const value = sprite[yo][xo]; + if (value.a == 0x00) continue; + + const dest = self.getUnsafe(x + xo, y + yo); + if (value.a == 0xFF) + dest.* = value + else + @panic("Not supported"); + }; +} + +pub fn clear(self: *Canvas, value: Pixel) void { + @memset(self.pixels, value); +} + +pub const Pixel = packed struct { + r: u8, + g: u8, + b: u8, + a: u8, + + pub const transparent = Pixel{ .r = 0x00, .g = 0x00, .b = 0x00, .a = 0x00 }; + pub const black = Pixel{ .r = 0x00, .g = 0x00, .b = 0x00, .a = 0xFF }; + pub const white = Pixel{ .r = 0xFF, .g = 0xFF, .b = 0xFF, .a = 0xFF }; +}; + +pub const Sprite = [16][16]Pixel; diff --git a/src/grid.zig b/src/grid.zig new file mode 100644 index 0000000..d19d53b --- /dev/null +++ b/src/grid.zig @@ -0,0 +1,51 @@ +const std = @import("std"); + +const Grid = @This(); + +pub const size = 16; + +array: [size][size][size]Tile = .{.{.{0} ** size} ** size} ** size, + +pub fn get(self: *Grid, pos: Pos) Ref { + return .{ .grid = self, .pos = pos }; +} + +pub fn layerAs(self: *Grid, z: i4, comptime T: type) []T { + const bytes = std.mem.sliceAsBytes(&self.array[@as(u4, @bitCast(z))]); + return std.mem.bytesAsSlice(T, bytes); +} + +pub const Tile = u8; + +pub const Pos = packed struct { + x: i4, + y: i4, + z: i4, + + pub fn init(x: i4, y: i4, z: i4) Pos { + return .{ .x = x, .y = y, .z = z }; + } + + pub fn initTruncate(x: anytype, y: anytype, z: anytype) Pos { + const sign = @typeInfo(@TypeOf(x, y, z)).Int.signedness; + const T = @Type(.{ .Int = .{ .signedness = sign, .bits = 4 } }); + return .{ + .x = @bitCast(@as(T, @truncate(x))), + .y = @bitCast(@as(T, @truncate(y))), + .z = @bitCast(@as(T, @truncate(z))), + }; + } +}; + +pub const Ref = struct { + grid: *Grid, + pos: Pos, + + pub fn as(self: Ref, comptime T: type) *T { + std.debug.assert(@sizeOf(T) == @sizeOf(Tile)); + const x: usize = @as(u4, @bitCast(self.pos.x)); + const y: usize = @as(u4, @bitCast(self.pos.y)); + const z: usize = @as(u4, @bitCast(self.pos.z)); + return @ptrCast(&self.grid.array[z][y][x]); + } +}; diff --git a/src/keys.zig b/src/keys.zig new file mode 100644 index 0000000..5c3fa09 --- /dev/null +++ b/src/keys.zig @@ -0,0 +1,112 @@ +const std = @import("std"); +const sdl = @import("sdl"); + +pub const PhysicalKey = enum(u8) { + // zig fmt: off + backspace = 0x08, + tab = '\t', + enter = '\r', + escape = 0x1B, + + // Numbers + _0 = '0', _1 = '1', _2 = '2', _3 = '3', _4 = '4', + _5 = '5', _6 = '6', _7 = '7', _8 = '8', _9 = '9', + + // Letters + a = 'A', b = 'B', c = 'C', d = 'D', e = 'E', f = 'F', g = 'G', + h = 'H', i = 'I', j = 'J', k = 'K', l = 'L', m = 'M', n = 'N', + o = 'O', p = 'P', q = 'Q', r = 'R', s = 'S', t = 'T', u = 'U', + v = 'V', w = 'W', x = 'X', y = 'Y', z = 'Z', + + // Visual + space = ' ', + grave = '`', + minus = '-', + equals = '=', + left_bracket = '[', + right_bracket = ']', + semicolon = ';', + apostrophe = '\'', + backslash = '\\', + comma = ',', + period = '.', + slash = '/', + + // Nagivation + insert = 0x8D, + delete = 0x7F, + home = 0x8E, + end = 0x9E, + page_up = 0x8F, + page_down = 0x9F, + + up = 0x8C, + down = 0x9C, + left = 0x9B, + right = 0x9D, + + // Modifiers + ctrl = 0x88, + alt = 0x89, + shift = 0x8A, + caps = 0x8B, + + _, + // zig fmt: on +}; + +pub const lookup = blk: { + const size = sdl.SDL_NUM_SCANCODES; + var result: [size]?PhysicalKey = .{null} ** size; + + result[sdl.SDL_SCANCODE_BACKSPACE] = PhysicalKey.backspace; + result[sdl.SDL_SCANCODE_TAB] = PhysicalKey.tab; + result[sdl.SDL_SCANCODE_RETURN] = PhysicalKey.enter; + result[sdl.SDL_SCANCODE_ESCAPE] = PhysicalKey.escape; + + for ('0'..'9' + 1) |i| { + const scancode = @field(sdl, "SDL_SCANCODE_" ++ .{i}); + const physical_key = @field(PhysicalKey, "_" ++ .{i}); + result[scancode] = physical_key; + } + + for ('A'..'Z' + 1) |i| { + const scancode = @field(sdl, "SDL_SCANCODE_" ++ .{i}); + const physical_key = @field(PhysicalKey, &.{std.ascii.toLower(i)}); + result[scancode] = physical_key; + } + + result[sdl.SDL_SCANCODE_GRAVE] = PhysicalKey.grave; + result[sdl.SDL_SCANCODE_MINUS] = PhysicalKey.minus; + result[sdl.SDL_SCANCODE_EQUALS] = PhysicalKey.equals; + result[sdl.SDL_SCANCODE_LEFTBRACKET] = PhysicalKey.left_bracket; + result[sdl.SDL_SCANCODE_RIGHTBRACKET] = PhysicalKey.right_bracket; + result[sdl.SDL_SCANCODE_SEMICOLON] = PhysicalKey.semicolon; + result[sdl.SDL_SCANCODE_APOSTROPHE] = PhysicalKey.apostrophe; + result[sdl.SDL_SCANCODE_BACKSLASH] = PhysicalKey.backslash; + result[sdl.SDL_SCANCODE_COMMA] = PhysicalKey.comma; + result[sdl.SDL_SCANCODE_PERIOD] = PhysicalKey.period; + result[sdl.SDL_SCANCODE_SLASH] = PhysicalKey.slash; + result[sdl.SDL_SCANCODE_SPACE] = PhysicalKey.space; + + result[sdl.SDL_SCANCODE_INSERT] = PhysicalKey.insert; + result[sdl.SDL_SCANCODE_DELETE] = PhysicalKey.delete; + result[sdl.SDL_SCANCODE_HOME] = PhysicalKey.home; + result[sdl.SDL_SCANCODE_END] = PhysicalKey.end; + result[sdl.SDL_SCANCODE_PAGEUP] = PhysicalKey.page_up; + result[sdl.SDL_SCANCODE_PAGEDOWN] = PhysicalKey.page_down; + result[sdl.SDL_SCANCODE_UP] = PhysicalKey.up; + result[sdl.SDL_SCANCODE_DOWN] = PhysicalKey.down; + result[sdl.SDL_SCANCODE_LEFT] = PhysicalKey.left; + result[sdl.SDL_SCANCODE_RIGHT] = PhysicalKey.right; + + result[sdl.SDL_SCANCODE_LCTRL] = PhysicalKey.ctrl; + result[sdl.SDL_SCANCODE_RCTRL] = PhysicalKey.ctrl; + result[sdl.SDL_SCANCODE_LALT] = PhysicalKey.alt; + result[sdl.SDL_SCANCODE_RALT] = PhysicalKey.alt; + result[sdl.SDL_SCANCODE_LSHIFT] = PhysicalKey.shift; + result[sdl.SDL_SCANCODE_RSHIFT] = PhysicalKey.shift; + result[sdl.SDL_SCANCODE_CAPSLOCK] = PhysicalKey.caps; + + break :blk result; +}; diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..bee6a19 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,92 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const sdl = @import("sdl"); +const Window = @import("./window.zig"); +const makeSdlError = Window.makeSdlError; + +const Canvas = @import("./canvas.zig"); +const Pixel = Canvas.Pixel; +const Sprite = Canvas.Sprite; + +const Grid = @import("./grid.zig"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + var prng = std.Random.DefaultPrng.init(0); + const random = prng.random(); + + var grid = Grid{}; + const key_layer = grid.layerAs(0, u8); + + var sprite_lookup: [256]Sprite = undefined; + try loadSprites(&sprite_lookup); + + const size = Grid.size * Grid.size; + + const scale = 3; + const window = try Window.init(allocator, size * scale, size * scale, key_layer); + defer window.deinit(); + + const canvas = try Canvas.init(allocator, size, size); + defer canvas.deinit(); + + while (window.running) { + window.pollEvents(); + + grid.get(Grid.Pos.init(-5, -4, 0)).as(u8).* +%= 1; + grid.get(Grid.Pos.init(-4, -4, 0)).as(u8).* = random.int(u8); + + render(&grid, canvas, &sprite_lookup); + try window.updateSurface(canvas); + } +} + +fn render(grid: *Grid, canvas: *Canvas, sprite_lookup: *[256]Sprite) void { + canvas.clear(Pixel.black); + + for (0..Grid.size) |vx| + for (0..Grid.size) |vy| + for (0..Grid.size) |vz| { + const x = @as(isize, @intCast(vx)) - Grid.size / 2; + const y = @as(isize, @intCast(vy)) - Grid.size / 2; + const z = @as(isize, @intCast(vz)) - Grid.size / 2; + + const pos = Grid.Pos.initTruncate(x, y, z); + const value = grid.get(pos).as(u8).*; + + const sprite = &sprite_lookup[value]; + canvas.putSprite(vx * Grid.size, vy * Grid.size, sprite); + }; +} + +fn loadSprites(sprite_lookup: *[256]Sprite) !void { + const png_surface = sdl.IMG_Load("./gfx/sprite_map.png") orelse + return makeSdlError(); + defer sdl.SDL_FreeSurface(png_surface); + + std.debug.assert(png_surface.w == 256); + std.debug.assert(png_surface.h == 256); + + const pixels_surface = sdl.SDL_ConvertSurfaceFormat( + png_surface, + sdl.SDL_PIXELFORMAT_ABGR8888, + 0, + ) orelse + return makeSdlError(); + defer sdl.SDL_FreeSurface(pixels_surface); + + const pixels_ptr: [*]Pixel = @alignCast(@ptrCast(pixels_surface.pixels)); + const pixels_len: usize = @intCast(pixels_surface.w * pixels_surface.h); + const pixels = pixels_ptr[0..pixels_len]; + + for (sprite_lookup, 0..) |*sprite, i| { + const x = i % 16; + const y = i / 16; + for (0..16) |yo| { + const dst_index = x * 16 + (y * 16 + yo) * 256; + @memcpy(&sprite[yo], pixels[dst_index..][0..16]); + } + } +} diff --git a/src/window.zig b/src/window.zig new file mode 100644 index 0000000..6c10a77 --- /dev/null +++ b/src/window.zig @@ -0,0 +1,105 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const sdl = @import("sdl"); +const keys = @import("./keys.zig"); +const PhysicalKey = keys.PhysicalKey; +const Canvas = @import("./canvas.zig"); + +const Window = @This(); + +allocator: Allocator, + +handle: *sdl.SDL_Window, +surface: ?*sdl.SDL_Surface = null, +key_status: []u8, +running: bool = true, + +pub fn init(allocator: Allocator, width: usize, height: usize, key_status: []u8) !*Window { + if (sdl.SDL_Init(sdl.SDL_INIT_AUDIO | sdl.SDL_INIT_VIDEO | sdl.SDL_INIT_EVENTS) < 0) + return makeSdlError(); + + const window = sdl.SDL_CreateWindow( + "Gridstep", + sdl.SDL_WINDOWPOS_UNDEFINED, + sdl.SDL_WINDOWPOS_UNDEFINED, + @intCast(width), + @intCast(height), + sdl.SDL_WINDOW_SHOWN | sdl.SDL_WINDOW_RESIZABLE, + ) orelse + return makeSdlError(); + + const result = try allocator.create(Window); + result.* = .{ + .allocator = allocator, + .handle = window, + .key_status = key_status, + }; + return result; +} + +pub fn deinit(self: *Window) void { + sdl.SDL_DestroyWindow(self.handle); + sdl.SDL_Quit(); + self.allocator.destroy(self); +} + +pub fn pollEvents(self: *Window) void { + { + // Special handling for the `.caps` key. + const i = @intFromEnum(PhysicalKey.caps); + const on = (sdl.SDL_GetModState() & sdl.KMOD_CAPS) != 0; + self.key_status[i] = if (on) i else 0; + } + + var ev = std.mem.zeroes(sdl.SDL_Event); + while (sdl.SDL_PollEvent(&ev) != 0) { + switch (ev.type) { + sdl.SDL_QUIT => { + self.running = false; + }, + sdl.SDL_WINDOWEVENT => { + switch (ev.window.event) { + sdl.SDL_WINDOWEVENT_RESIZED => { + // Invalidate surface when window is resized. + self.surface = null; + }, + else => {}, + } + }, + sdl.SDL_KEYDOWN, sdl.SDL_KEYUP => { + const key = ev.key.keysym; + const down = (ev.type == sdl.SDL_KEYDOWN); + if (keys.lookup[key.scancode]) |k| { + if (k == .caps) break; + const i = @intFromEnum(k); + self.key_status[i] = if (down) i else 0; + } + }, + else => {}, + } + } +} + +pub fn updateSurface(self: *Window, canvas: *Canvas) !void { + // If surface has not yet been created or invalidated due + // to window being resized, reaquire the window's surface. + if (self.surface == null) + self.surface = sdl.SDL_GetWindowSurface(self.handle) orelse + return makeSdlError(); + + const w: c_int = @intCast(canvas.width); + const h: c_int = @intCast(canvas.height); + const src_rect = sdl.SDL_Rect{ .x = 0, .y = 0, .w = w, .h = h }; + var dest_rect = sdl.SDL_Rect{ .x = 0, .y = 0, .w = self.surface.?.w, .h = self.surface.?.h }; + if (sdl.SDL_BlitScaled(canvas.surface, &src_rect, self.surface, &dest_rect) < 0) + return makeSdlError(); + + if (sdl.SDL_UpdateWindowSurface(self.handle) < 0) + return makeSdlError(); +} + +pub fn makeSdlError() error{SdlError} { + std.debug.print("{s}\n", .{sdl.SDL_GetError()}); + return error.SdlError; +}