diff options
author | alaric <alaric@netmythos.org> | 2024-04-06 05:35:32 -0700 |
---|---|---|
committer | alaric <alaric@netmythos.org> | 2024-04-06 05:35:32 -0700 |
commit | b614db3cf2a89f565b06471dc47e1bd345c0b070 (patch) | |
tree | bbda227d10290bc7c28f68c2768fb3d87d696279 | |
download | snake-b614db3cf2a89f565b06471dc47e1bd345c0b070.tar.gz snake-b614db3cf2a89f565b06471dc47e1bd345c0b070.zip |
Made snake.
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | build.zig | 37 | ||||
-rw-r--r-- | build.zig.zon | 62 | ||||
-rw-r--r-- | flake.lock | 146 | ||||
-rw-r--r-- | flake.nix | 48 | ||||
-rw-r--r-- | src/fragment.glsl | 12 | ||||
-rw-r--r-- | src/geometry.zig | 11 | ||||
-rw-r--r-- | src/grid_vertex.glsl | 22 | ||||
-rw-r--r-- | src/matrix.zig | 141 | ||||
-rw-r--r-- | src/shell.html | 136 | ||||
-rw-r--r-- | src/vertex.glsl | 14 | ||||
-rw-r--r-- | src/wasm.zig | 34 | ||||
-rw-r--r-- | src/wasm_snake.zig | 330 | ||||
-rw-r--r-- | src/webgl.js | 283 | ||||
-rw-r--r-- | src/webgl.zig | 302 | ||||
-rw-r--r-- | src/webgl_bindings.zig | 157 |
16 files changed, 1736 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c80a22 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +zig-* diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..0da779f --- /dev/null +++ b/build.zig @@ -0,0 +1,37 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.resolveTargetQuery(.{ + .cpu_arch = .wasm32, + .os_tag = .freestanding, + }); + const optimize = b.standardOptimizeOption(.{}); + + b.installFile("src/shell.html", "index.html"); + b.installFile("src/webgl.js", "webgl.js"); + + const exe = b.addExecutable(.{ + .name = "snake", + .root_source_file = .{ .path = "src/wasm_snake.zig" }, + .target = target, + .optimize = optimize, + }); + exe.entry = .disabled; + exe.root_module.export_symbol_names = &[_][]const u8{ + "init", + "update", + }; + + b.installArtifact(exe); + + const exe_unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/matrix.zig" }, + .target = b.resolveTargetQuery(.{}), + .optimize = optimize, + }); + + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_exe_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..c02485f --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,62 @@ +.{ + .name = "snake", + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + + // This field is optional. + // This is currently advisory only; Zig does not yet do anything + // with this value. + //.minimum_zig_version = "0.11.0", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save <url>` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + //}, + }, + + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + // This makes *all* files, recursively, included in this package. It is generally + // better to explicitly list the files and directories instead, to insure that + // fetching from tarballs, file system paths, and version control all result + // in the same contents hash. + "", + // For example... + //"build.zig", + //"build.zig.zon", + //"src", + //"LICENSE", + //"README.md", + }, +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..a2a4e01 --- /dev/null +++ b/flake.lock @@ -0,0 +1,146 @@ +{ + "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_2": { + "flake": false, + "locked": { + "lastModified": 1673956053, + "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "locked": { + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1704290814, + "narHash": "sha256-LWvKHp7kGxk/GEtlrGYV68qIvPHkU9iToomNFGagixU=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "70bdadeb94ffc8806c0570eb5c2695ad29f0e421", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-23.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1702350026, + "narHash": "sha256-A+GNZFZdfl4JdDphYKBJ5Ef1HOiFsP18vQe9mqjmUis=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9463103069725474698139ab10f17a9d125da859", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-23.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "zig": "zig" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "zig": { + "inputs": { + "flake-compat": "flake-compat_2", + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1712276574, + "narHash": "sha256-5oDzy7J7KLVWh69usFrD/r9B4CBRtK6X5MsYTNjMysk=", + "owner": "mitchellh", + "repo": "zig-overlay", + "rev": "e09d925aef8eea36cbac36445c22ec7fcfbcaac6", + "type": "github" + }, + "original": { + "owner": "mitchellh", + "repo": "zig-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..e1e9d5e --- /dev/null +++ b/flake.nix @@ -0,0 +1,48 @@ +{ + description = "The flake for Snake"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-23.05"; + flake-utils.url = "github:numtide/flake-utils"; + zig.url = "github:mitchellh/zig-overlay"; + + # Used for shell.nix + flake-compat = { + url = "github:edolstra/flake-compat"; + flake = false; + }; + }; + + outputs = { + self, + nixpkgs, + flake-utils, + ... + } @ inputs: let + overlays = [ + # Other overlays + (final: prev: { + zigpkgs = inputs.zig.packages.${prev.system}; + }) + ]; + + # Our supported systems are the same supported systems as the Zig binaries + systems = builtins.attrNames inputs.zig.packages; + in + flake-utils.lib.eachSystem systems ( + system: let + pkgs = import nixpkgs {inherit overlays system;}; + in { + devShells.default = pkgs.mkShell { + nativeBuildInputs = with pkgs; [ + zigpkgs.master + python3 + ]; + }; + + # For compatibility with older versions of the `nix` binary + devShell = self.devShells.${system}.default; + } + ); +} + diff --git a/src/fragment.glsl b/src/fragment.glsl new file mode 100644 index 0000000..c1a126d --- /dev/null +++ b/src/fragment.glsl @@ -0,0 +1,12 @@ +#version 300 es +precision highp float; + +in vec4 a_col; + +uniform vec3 color; + +out vec4 outColor; + +void main() { + outColor = a_col * vec4(color, 1); +} diff --git a/src/geometry.zig b/src/geometry.zig new file mode 100644 index 0000000..b4245c9 --- /dev/null +++ b/src/geometry.zig @@ -0,0 +1,11 @@ +pub fn planeVertices(width: f32, height: f32) [18]f32 { + return .{ + 0, 0, 0, + width, 0, 0, + 0, height, 0, + + 0, height, 0, + width, 0, 0, + width, height, 0, + }; +} diff --git a/src/grid_vertex.glsl b/src/grid_vertex.glsl new file mode 100644 index 0000000..2cfb4f0 --- /dev/null +++ b/src/grid_vertex.glsl @@ -0,0 +1,22 @@ +#version 300 es + + +uniform mat3 matrix; +uniform float scale; +uniform float gap; +uniform int columns; +uniform vec2 root; +uniform vec3 color; + +out vec4 a_col; + +void main() { + float row = float(gl_InstanceID / columns); + float col = float(gl_InstanceID % columns); + vec2 offset = vec2(col * scale + gap * col, row * scale + gap * row) + root; + vec2 verts[] = vec2[]( vec2(0, 0), vec2(1, 0), vec2(0, 1), + vec2(0, 1), vec2(1, 0), vec2(1, 1) ); + vec2 p = (verts[gl_VertexID] * scale) + offset; + gl_Position = vec4((matrix * vec3(p, 1)).xy, 0, 1); + a_col = vec4(color, 1); +} diff --git a/src/matrix.zig b/src/matrix.zig new file mode 100644 index 0000000..485f2f1 --- /dev/null +++ b/src/matrix.zig @@ -0,0 +1,141 @@ +pub fn Matrix(comptime T: type, row_count: usize, col_count: usize) type { + return struct { + pub const ValT = T; + pub const rows = row_count; + pub const cols = col_count; + pub const RowT = [cols]T; + pub const ColT = [rows]T; + const Self = @This(); + + pub const identity: Self = blk: { + const fill = if (T == bool) true else 1; + const empty = if (T == bool) false else 0; + var r: [cols]@Vector(rows, T) = undefined; + for (0..cols) |c| { + var irow: RowT = [1]T{empty} ** rows; + irow[c] = fill; + r[c] = irow; + } + break :blk .{ .data = r }; + }; + + data: [cols]ColT, + + pub fn multiply(self: Self, other: anytype) MatMultResult(Self, @TypeOf(other)) { + const OtherT = @TypeOf(other); + comptime assert(Self.cols == OtherT.rows); + const ResultT = MatMultResult(Self, @TypeOf(other)); + var result: ResultT = undefined; + for (0..ResultT.rows) |ri| { + const r: @Vector(Self.cols, T) = self.row(ri); + for (0..ResultT.cols) |ci| { + const c: @Vector(OtherT.rows, T) = other.data[ci]; + result.data[ci][ri] = @reduce(.Add, r * c); + } + } + return result; + } + + pub fn translation(t: [rows - 1]T) Self { + var base = Self.identity; + for (0..rows - 1) |ri| { + base.data[cols - 1][ri] = t[ri]; + } + return base; + } + + pub fn xRotation(angle: T) Self { + if (rows < 3 or cols < 3) @compileError("Called xRotation on a Matrix smaller than 3x3"); + const c = @cos(angle); + const s = @sin(angle); + var base = Self.identity; + base.data[1][1] = c; + base.data[2][2] = c; + base.data[2][1] = s; + base.data[1][2] = -s; + return base; + } + + pub fn yRotation(angle: T) Self { + if (rows < 3 or cols < 3) @compileError("Called yRotation on a Matrix smaller than 3x3"); + const c = @cos(angle); + const s = @sin(angle); + var base = Self.identity; + base.data[0][0] = c; + base.data[2][2] = c; + base.data[2][0] = s; + base.data[0][2] = -s; + return base; + } + + pub fn zRotation(angle: T) Self { + if (rows < 2 or cols < 2) @compileError("Called rotation on a Matrix smaller than 2x2"); + const c = @cos(angle); + const s = @sin(angle); + var base = Self.identity; + base.data[0][0] = c; + base.data[0][1] = -s; + base.data[1][0] = s; + base.data[1][1] = c; + return base; + } + + pub fn scale(vals: [@min(rows, cols) - 1]f32) Self { + var r = identity; + for (vals, 0..) |v, i| { + r.data[i][i] = v; + } + return r; + } + + pub fn row(self: Self, ri: usize) RowT { + var r: RowT = undefined; + for (0..cols) |ci| { + r[ci] = self.data[ci][ri]; + } + return r; + } + + pub fn col(self: Self, ci: usize) ColT { + const start = ci * rows; + return self.data[start .. start + cols]; + } + + pub fn equals(self: Self, other: Self) bool { + for (self.data, other.data) |ac, bc| { + for (ac, bc) |a, b| { + if (a != b) return false; + } + } + return true; + } + }; +} + +fn MatMultResult(a: type, b: type) type { + if (a.cols != b.rows) { + @compileLog("Matrix Multiplication requires that the number of columns in the lhs equals the number of rows in the rhs.", a, b); + @compileError("Matrix multiplication with mismatched dimensions"); + } + + return Matrix(a.ValT, a.rows, b.cols); +} + +fn assert(ok: bool) void { + if (!ok) unreachable; +} + +test "translation" { + const testing = @import("std").testing; + const Mat3 = Matrix(f32, 3, 3); + const x = 13; + const y = -8; + const tlate = Mat3.translation(.{ x, y }); + const zero = [3]f32{ 0, 0, 1 }; + const expected_bits = [3]f32{ x, y, 1 }; + + const in_col = Matrix(f32, 3, 1){ .data = @bitCast(zero) }; + const expected_col = Matrix(f32, 3, 1){ .data = @bitCast(expected_bits) }; + const actual_col = tlate.multiply(in_col); + try testing.expectEqual(expected_col, actual_col); +} diff --git a/src/shell.html b/src/shell.html new file mode 100644 index 0000000..3d84bf2 --- /dev/null +++ b/src/shell.html @@ -0,0 +1,136 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Snake</title> + <style> + body { + overflow: hidden; + min-width: 1280px; + min-height: 720px; + display: flex; + flex-direction: column; + margin: 0; + } + #app-canvas { + display: flex; + flex-grow: 1; + width: 1280px; + max-height: 720px; + height: 100%; + border: solid 1px black; + } + </style> + </head> + <body> + <canvas id="app-canvas" tabindex="0"></canvas> + <script src="webgl.js"></script> + <script> + canvas.width = canvas.clientWidth; + canvas.height = canvas.clientHeight; + let memory = null; + let last_frame_time = Date.now(); + + const writeString = (str, pointer, length) => { + if (!memory) { + return null; + } + + const l = Math.min(length, str.length); + const from = new TextEncoder().encode(str.slice(0, l)); + const to = new Uint8Array(memory.buffer, pointer, length); + to.set(from); + return l; + }; + + const readString = (pointer, length) => { + if (!memory) { + return null; + } + + // Memory in WASM is one big buffer. We can read a string from the + // Zig/WASM space if we know the pointer and length. + return new TextDecoder().decode( + new Uint8Array(memory.buffer, pointer, length), + ); + }; + + + const consoleLog = (ptr, len) => { + console.log(readString(ptr, len)); + } + + const rand = () => { + return Math.random(); + }; + + const loadTexture = (ptr, len, tex_id, ret_ptr) => { + const path = readString(ptr, len); + var image = new Image(); + image.src = path; + console.log("loading ", path); + image.addEventListener('load', function() { + const ret_list = new Uint32Array(memory.buffer, ret_ptr, 3); + ret_list[0] = 1; + ret_list[1] = image.width; + ret_list[2] = image.height; + console.log(path, " loaded"); + const tex = glTextures.get(tex_id); + gfx_ctx.bindTexture(gfx_ctx.TEXTURE_2D, tex); + gfx_ctx.texImage2D(gfx_ctx.TEXTURE_2D, 0, gfx_ctx.RGBA, gfx_ctx.RGBA, gfx_ctx.UNSIGNED_BYTE, image); + gfx_ctx.generateMipmap(gfx_ctx.TEXTURE_2D); + }); + }; + + const keyRegistry = new Map(); + canvas.addEventListener("keydown", (e) => { + const p = keyRegistry.get(e.code); + if (p) { + p[0] = 1; + } + }); + + canvas.addEventListener("keyup", (e) => { + const p = keyRegistry.get(e.code); + if (p) { + p[0] = 0; + } + }); + + const registerKeyInput = (ptr, len, out_ptr) => { + const code = readString(ptr, len); + const list = new Uint8Array(memory.buffer, out_ptr, 1); + keyRegistry.set(code, list); + }; + + const importObject = { + env: { + ...webgl, + consoleLog, + rand, + loadTexture, + registerKeyInput, + }, + }; + + WebAssembly.instantiateStreaming(fetch("bin/snake.wasm"), importObject).then( + (obj) => { + memory = obj.instance.exports.memory; + obj.instance.exports.init(); + + const update = obj.instance.exports.update; + function step() { + const time = Date.now(); + const elapsed = time - last_frame_time; + update(elapsed / 1000); + last_frame_time = time; + window.requestAnimationFrame(step); + } + + window.requestAnimationFrame(step); + } + ); + </script> + </body> +</html> diff --git a/src/vertex.glsl b/src/vertex.glsl new file mode 100644 index 0000000..874784a --- /dev/null +++ b/src/vertex.glsl @@ -0,0 +1,14 @@ +#version 300 es + +uniform mat3 projection; +uniform mat3 model; +uniform vec3 color; + +in vec2 pos; + +out vec4 a_col; + +void main() { + gl_Position = vec4((projection * model * vec3(pos, 1)).xy, 0, 1); + a_col = vec4(color, 1); +} diff --git a/src/wasm.zig b/src/wasm.zig new file mode 100644 index 0000000..776097c --- /dev/null +++ b/src/wasm.zig @@ -0,0 +1,34 @@ +//! The zig interface for accessing functions from the web/javascript +//! side of the engine. Whenever the engine has a more concrete identity, +//! this should probably get a more expressive filename. + +const std = @import("std"); + +extern fn consoleLog(ptr: [*c]const u8, len: usize) void; +extern fn registerKeyInput(ptr: [*c]const u8, len: u32, out: *bool) void; +pub extern fn rand() f32; + +pub fn print(comptime fmt: []const u8, args: anytype) void { + var buf: [512]u8 = undefined; + const msg = std.fmt.bufPrint(&buf, fmt, args) catch "There was an error formatting your output string"; + consoleLog(msg.ptr, msg.len); +} + +pub const KeyboardKey = enum { + up, + down, + left, + right, + c, +}; + +pub fn registerKey(key: KeyboardKey, out: *bool) void { + const key_code = switch (key) { + .up => "ArrowUp", + .down => "ArrowDown", + .left => "ArrowLeft", + .right => "ArrowRight", + .c => "KeyC", + }; + registerKeyInput(key_code.ptr, key_code.len, out); +} diff --git a/src/wasm_snake.zig b/src/wasm_snake.zig new file mode 100644 index 0000000..22e7258 --- /dev/null +++ b/src/wasm_snake.zig @@ -0,0 +1,330 @@ +const wasm = @import("wasm.zig"); +const webgl = @import("webgl.zig"); +const geometry = @import("geometry.zig"); +const matrix = @import("matrix.zig"); +const Mat3 = matrix.Matrix(f32, 3, 3); +const Mat4 = matrix.Matrix(f32, 4, 4); + +const GridProg = webgl.Program( + &.{ + .{ .identifier = "matrix", .shader_name = "matrix", .kind = .mat3 }, + .{ .identifier = "scale", .shader_name = "scale", .kind = .f32 }, + .{ .identifier = "columns", .shader_name = "columns", .kind = .i32 }, + .{ .identifier = "gap", .shader_name = "gap", .kind = .f32 }, + .{ .identifier = "root", .shader_name = "root", .kind = .vec2 }, + .{ .identifier = "color", .shader_name = "color", .kind = .vec3 }, + }, + &.{}, +); + +const GeneralProg = webgl.Program( + &.{ + .{ .identifier = "projection", .shader_name = "projection", .kind = .mat3 }, + .{ .identifier = "model", .shader_name = "model", .kind = .mat3 }, + .{ .identifier = "color", .shader_name = "color", .kind = .vec3 }, + }, + &.{ + .{ .identifier = "pos", .shader_name = "pos", .kind = .vec3 }, + }, +); + +var general_prog: GeneralProg = undefined; +var grid_prog: GridProg = undefined; + +var vao: webgl.VertexArrayObject = undefined; +var pos_buffer: webgl.Buffer = undefined; + +const grid = .{ + .origin = .{ 100, 100 }, + .gap = 4, + .width = 16, + .height = 8, + .scale = 64, +}; + +const RingBuffer = struct { + data: [][2]i32, + len: usize = 0, + write_idx: usize = 0, + + fn write(self: *RingBuffer, val: [2]i32) void { + if (self.len == 0) return; + + self.data[self.write_idx] = val; + self.write_idx = (self.write_idx + 1) % self.len; + } + + fn oldest(self: RingBuffer) [2]i32 { + const i = (self.data.len + self.write_idx - self.len) % self.data.len; + return self.data[i]; + } +}; + +const max_len = 40; +var segment_mem: [max_len][2]i32 = [1][2]i32{.{ 0, 0 }} ** max_len; + +var snake: struct { x: i32, y: i32, dir: [2]i32, len: usize } = .{ + .x = 0, + .y = 0, + .dir = .{ 0, -1 }, + .len = 1, +}; + +var food: struct { x: i32, y: i32 } = .{ + .x = 4, + .y = 4, +}; + +var rng: LinearCongruentialGenerator = undefined; + +var up: bool = false; +var p_up: bool = false; +var down: bool = false; +var p_down: bool = false; +var left: bool = false; +var p_left: bool = false; +var right: bool = false; +var p_right: bool = false; + +const plane_verts = geometry.planeVertices(1, 1); + +export fn init() void { + wasm.print("Hello Snake {s}!", .{"sssss"}); + wasm.registerKey(.up, &up); + wasm.registerKey(.down, &down); + wasm.registerKey(.left, &left); + wasm.registerKey(.right, &right); + + const f_seed: f32 = wasm.rand() * (pow(f32, 2, 16) - 1); + const seed: u64 = @intFromFloat(f_seed); + rng = LinearCongruentialGenerator.ZX81(seed); + food.x = rng.randInt(i32, 0, grid.width - 1); + food.y = rng.randInt(i32, 0, grid.height - 1); + + general_prog = GeneralProg.init( + @embedFile("vertex.glsl"), + @embedFile("fragment.glsl"), + ) catch return; + + grid_prog = GridProg.init( + @embedFile("grid_vertex.glsl"), + @embedFile("fragment.glsl"), + ) catch return; + + pos_buffer = webgl.Buffer.create(); + pos_buffer.bindAndFill(.array_buffer, .vec3, toVec3Array(&plane_verts), .static_draw); + + vao = webgl.VertexArrayObject.init(); + general_prog.setVertexAttribPointer(.pos, vao, pos_buffer); +} + +const tick_rate = 1.0 / 60.0; +var timer: f32 = 0; + +const ticks_per_move = 12; +var counter: u8 = 0; + +export fn update(delta_time: f32) void { + if (up and !p_up and snake.dir[1] != -1) { + snake.dir = .{ 0, 1 }; + } + + if (down and !p_down and snake.dir[1] != 1) { + snake.dir = .{ 0, -1 }; + } + + if (right and !p_right and snake.dir[0] != -1) { + snake.dir = .{ 1, 0 }; + } + + if (left and !p_left and snake.dir[0] != 1) { + snake.dir = .{ -1, 0 }; + } + + timer += delta_time; + while (timer > tick_rate) { + counter += 1; + timer -= tick_rate; + + if (counter >= ticks_per_move) { + counter -= ticks_per_move; + snake.x += snake.dir[0]; + snake.x = @mod(snake.x, grid.width); + + snake.y += snake.dir[1]; + snake.y = @mod(snake.y, grid.height); + + if (snake.x == food.x and snake.y == food.y) { + food.x = rng.randInt(i32, 0, grid.width - 1); + food.y = rng.randInt(i32, 0, grid.height - 1); + + if (snake.len < max_len) { + snake.len += 1; + } + } + + var i = snake.len - 1; + while (i > 0) : (i -= 1) { + segment_mem[i] = segment_mem[i - 1]; + if (segment_mem[i][0] == snake.x and segment_mem[i][1] == snake.y) { + snake.len = 1; + break; + } + } + segment_mem[0] = .{ snake.x, snake.y }; + } + } + + webgl.viewportToScreen(); + const bg: webgl.Color = comptime webgl.Color.fromVec(.{ 0, 0.4, 0.4, 1 }); + webgl.clear(bg); + drawGrid(); + drawEntities(); + + p_up = up; + p_down = down; + p_left = left; + p_right = right; +} + +fn drawGrid() void { + grid_prog.use(); + grid_prog.setUniform(.matrix, comptime projectionMatrix2D(1280, 720)); + grid_prog.setUniform(.scale, grid.scale); + grid_prog.setUniform(.columns, grid.width); + grid_prog.setUniform(.gap, grid.gap); + grid_prog.setUniform(.root, grid.origin); + grid_prog.setUniform(.color, .{ 0, 0.3, 0.3 }); + webgl.drawArraysInstanced(.triangles, 0, 6, grid.height * grid.width); +} + +fn drawEntities() void { + general_prog.use(); + const proj = comptime projectionMatrix2D(1280, 720); + general_prog.setUniform(.projection, proj); + + // Snake + for (0..snake.len) |i| { + const coord = segment_mem[i]; + const fx: f32 = @floatFromInt(coord[0]); + const fy: f32 = @floatFromInt(coord[1]); + + const pos = @as(@Vector(2, f32), grid.origin) + @Vector(2, f32){ + fx * grid.gap + fx * grid.scale, + fy * grid.gap + fy * grid.scale, + }; + const tlate = Mat3.translation(pos); + const scale = Mat3.scale(.{ grid.scale, grid.scale }); + const tform = tlate.multiply(scale); + general_prog.setUniform(.model, tform); + general_prog.setUniform(.color, .{ 0.7, 0.3, 0.3 }); + + webgl.drawArrays(.triangles, 0, 6); + } + + { + const x: f32 = @floatFromInt(food.x); + const y: f32 = @floatFromInt(food.y); + const pos = @as(@Vector(2, f32), grid.origin) + @Vector(2, f32){ + x * grid.gap + x * grid.scale, + y * grid.gap + y * grid.scale, + }; + const tlate = Mat3.translation(pos); + const scale = Mat3.scale(.{ grid.scale, grid.scale }); + const tform = tlate.multiply(scale); + general_prog.setUniform(.model, tform); + general_prog.setUniform(.color, .{ 0.0, 0.7, 0.3 }); + + webgl.drawArrays(.triangles, 0, 6); + } +} + +comptime {} + +fn projectionMatrix2D(width: f32, height: f32) Mat3 { + const mat_vals = .{ + .{ 2.0 / width, 0, 0 }, + .{ 0, 2.0 / height, 0 }, + .{ -1, -1, 1 }, + }; + return .{ .data = mat_vals }; +} + +fn toVec3Array(slice: []const f32) []const [3]f32 { + const new_len = slice.len / 3; + const many: [*]const [3]f32 = @ptrCast(slice.ptr); + return many[0..new_len]; +} + +fn assert(ok: bool) void { + if (!ok) unreachable; +} + +pub fn pow(comptime T: type, base: T, exp: T) T { + if (exp == 0) { + return 1; + } else if (exp > 0) { + return base * pow(T, base, exp - 1); + } else if (exp < 0) { + return pow(T, base, exp + 1) / base; + } + unreachable; +} + +pub const LinearCongruentialGenerator = struct { + mod: u64, + mul: u64, + inc: u64, + seed: u64, + + const Self = @This(); + /// Move to the next seed value internally and return that value + pub fn next(self: *Self) u64 { + const r = (self.seed *% self.mul +% self.inc) % self.mod; + self.seed = r; + return r; + } + + /// Generate an int of type T with a value from min to max (inclusive) + pub fn randInt(self: *Self, comptime T: type, min: T, max: T) T { + assert(max > min); + const range: u64 = @as(u64, @intCast(max - min)) + 1; + assert(self.mod >= range); + const val: T = @intCast(self.next() % range); + return min + val; + } + + pub fn randFloat(self: *Self, comptime T: type) T { + const pct: T = @as(T, @floatFromInt(self.next())) / @as(T, @floatFromInt(self.mod)); + return pct; + } + + pub fn randEnum(self: *Self, comptime T: type) T { + const info = @typeInfo(T); + if (info != .Enum) @compileError("Cannot call randEnum on type " ++ @typeName(T)); + const fields = info.Enum.fields; + const vals = comptime blk: { + var result: [fields.len]T = undefined; + for (fields, 0..) |f, i| { + result[i] = @field(T, f.name); + } + break :blk result; + }; + const i = self.randInt(usize, 0, fields.len - 1); + return vals[i]; + } + + pub fn randBool(self: *Self) bool { + return self.next() % 2 == 0; + } + + pub fn ZX81(seed: u64) Self { + const mod = pow(u64, 2, 16) + 1; + return .{ + .seed = seed, + .mod = mod, + .mul = 75, + .inc = 74, + }; + } +}; diff --git a/src/webgl.js b/src/webgl.js new file mode 100644 index 0000000..7b14d22 --- /dev/null +++ b/src/webgl.js @@ -0,0 +1,283 @@ +const canvas = document.getElementById("app-canvas"); +const gfx_ctx = canvas.getContext("webgl2"); +if (gfx_ctx === null) { + alert("There was a problem initializing WebGL. Your browser or machine may not support it."); +} + +let next_id = 0; +const getId = () => { + next_id += 1; + return next_id - 1; +}; + +const glBuffers = new Map(); +const glShaders = new Map(); +const glPrograms = new Map(); +const glVertexArrays = new Map(); +const glUniformLocations = new Map(); +const glTextures = new Map(); + +const attachShader = (program_id, shader_id) => { + const program = glPrograms.get(program_id); + const shader = glShaders.get(shader_id); + gfx_ctx.attachShader(program, shader); +}; + +const blendFunc = (sfactor, dfactor) => { + gfx_ctx.blendFunc(sfactor, dfactor); +}; + +const bindBuffer = (target, id) => { + gfx_ctx.bindBuffer(target, glBuffers.get(id)); +}; + +const bufferData = (target, ptr, len, usage) => { + const floats = new Float32Array(memory.buffer, ptr, len); + gfx_ctx.bufferData(target, floats, usage); +}; + +const bindVertexArray = (vao) => { + gfx_ctx.bindVertexArray(glVertexArrays.get(vao)); +}; + +const createVertexArray = () => { + const id = getId(); + glVertexArrays.set(id, gfx_ctx.createVertexArray()); + return id; +}; + +const clearColor = (r, g, b, a) => { + gfx_ctx.clearColor(r, g, b, a); +}; + +const clear = (mask) => { + gfx_ctx.clear(mask); +}; + +const createBuffer = () => { + const id = getId(); + glBuffers.set(id, gfx_ctx.createBuffer()); + return id; +}; + +const createShader = (type) => { + const id = getId(); + glShaders.set(id, gfx_ctx.createShader(type)); + return id; +}; + +const compileShader = (shader) => { + gfx_ctx.compileShader(glShaders.get(shader)); +}; + +const createProgram = (type) => { + const id = getId(); + glPrograms.set(id, gfx_ctx.createProgram()); + return id; +}; + +const deleteShader = (shader) => { + gfx_ctx.deleteShader(glShaders.get(shader)); +}; + +const deleteProgram = (program) => { + gfx_ctx.deleteProgram(glPrograms.get(program)); +}; + +const enable = (cap) => { + gfx_ctx.enable(cap); +}; + + +const enableVertexAttribArray = (attrib_location) => { + gfx_ctx.enableVertexAttribArray(attrib_location); +}; + +const getShaderParameter = (shader, info) => { + return gfx_ctx.getShaderParameter(glShaders.get(shader), info); +}; + +const getShaderInfoLog = (shader, ptr, len) => { + const msg = gfx_ctx.getShaderInfoLog(glShaders.get(shader)); + const r = writeString(msg, ptr, len); + return r; +}; + +const getProgramParameter = (program_id, info) => { + const program = glPrograms.get(program_id); + return gfx_ctx.getProgramParameter(program, info); +}; + +const getProgramInfoLog = (program, ptr, len) => { + const msg = gfx_ctx.getProgramInfoLog(glPrograms.get(program)); + const r = writeString(msg, ptr, len); + return r; +}; + +const getAttribLocation = (program, ptr, len) => { + const attrib = readString(ptr, len); + return gfx_ctx.getAttribLocation(glPrograms.get(program), attrib); +}; + +const getUniformLocation = (program, ptr, len) => { + const uniform = readString(ptr, len); + const loc = gfx_ctx.getUniformLocation(glPrograms.get(program), uniform); + if (!loc) console.log("Uniform ", uniform, " could not be found"); + if (!loc) return -1; + + const id = getId(); + glUniformLocations.set(id, loc); + return id; +}; + +const linkProgram = (program) => { + gfx_ctx.linkProgram(glPrograms.get(program)); +}; + +const shaderSource = (shader, ptr, len) => { + const source = readString(ptr, len); + gfx_ctx.shaderSource(glShaders.get(shader), source); +}; + +const useProgram = (prog) => { + gfx_ctx.useProgram(glPrograms.get(prog)); +}; + +const uniform1i = (location, v0) => { + gfx_ctx.uniform1i(glUniformLocations.get(location), v0); +}; + +const uniform1f = (location, v0) => { + gfx_ctx.uniform1f(glUniformLocations.get(location), v0); +}; + +const uniform2f = (location, v0, v1) => { + gfx_ctx.uniform2f(glUniformLocations.get(location), v0, v1); +}; + +const uniform3f = (location, v0, v1, v2) => { + gfx_ctx.uniform3f(glUniformLocations.get(location), v0, v1, v2); +}; + +const uniform4f = (location, v0, v1, v2, v3) => { + gfx_ctx.uniform4f(glUniformLocations.get(location), v0, v1, v2, v3); +}; + +const uniformMatrix3fv = (location, ptr) => { + const u = glUniformLocations.get(location); + const floats = new Float32Array(memory.buffer, ptr, 9); + + gfx_ctx.uniformMatrix3fv(glUniformLocations.get(location), false, floats); +}; + +const uniformMatrix4fv = (location, ptr) => { + const u = glUniformLocations.get(location); + const floats = new Float32Array(memory.buffer, ptr, 16); + + gfx_ctx.uniformMatrix4fv(glUniformLocations.get(location), false, floats); +}; + +const vertexAttribPointer = (loc, size, type, normalize, stride, offset) => { + gfx_ctx.vertexAttribPointer(loc, size, type, normalize, stride, offset); +}; + +const viewport = (x, y, width, height) => { + gfx_ctx.viewport(x, y, width, height); +}; + +const drawArrays = (mode, first, count) => { + gfx_ctx.drawArrays(mode, first, count); +}; + +const createTexture = () => { + const id = getId(); + glTextures.set(id, gfx_ctx.createTexture()); + return id; +}; + +const bindTexture = (target, tex_id) => { + const texture = glTextures.get(tex_id); + gfx_ctx.bindTexture(target, texture); +}; + +const texImage2D = (target, level, interal_format, + width, height, border, format, + data_type, ptr, count, offset) => { + const data = new Uint8Array(memory.buffer, ptr, count); + gfx_ctx.texImage2D(target, level, interal_format, width, height, border, format, data_type, data, offset); +}; + +const activeTexture = (tex_id) => { + gfx_ctx.activeTexture(tex_id); +}; + +const vertexAttribDivisor = (loc, divisor) => { + gfx_ctx.vertexAttribDivisor(loc, divisor); +}; + +const drawArraysInstanced = (mode, first, count, instanceCount) => { + gfx_ctx.drawArraysInstanced(mode, first, count, instanceCount); +}; + +const getScreenWidth = () => { + return gfx_ctx.drawingBufferWidth; +}; + +const getScreenHeight = () => { + return gfx_ctx.drawingBufferHeight; +}; + +const webgl = { + bindBuffer, + blendFunc, + bufferData, + + createShader, + clear, + clearColor, + createBuffer, + + enable, + + shaderSource, + compileShader, + getShaderParameter, + deleteShader, + getShaderInfoLog, + + createProgram, + deleteProgram, + attachShader, + linkProgram, + getProgramParameter, + getProgramInfoLog, + useProgram, + + getAttribLocation, + getUniformLocation, + createVertexArray, + bindVertexArray, + enableVertexAttribArray, + vertexAttribPointer, + viewport, + drawArrays, + + uniform1i, + uniform1f, + uniform2f, + uniform3f, + uniform4f, + uniformMatrix3fv, + uniformMatrix4fv, + + createTexture, + bindTexture, + texImage2D, + activeTexture, + + vertexAttribDivisor, + drawArraysInstanced, + + getScreenWidth, + getScreenHeight, +}; diff --git a/src/webgl.zig b/src/webgl.zig new file mode 100644 index 0000000..e252286 --- /dev/null +++ b/src/webgl.zig @@ -0,0 +1,302 @@ +const std = @import("std"); +const wasm = @import("wasm.zig"); +const bindings = @import("webgl_bindings.zig"); +const matrix = @import("matrix.zig"); +const Mat3 = matrix.Matrix(f32, 3, 3); +const Mat4 = matrix.Matrix(f32, 4, 4); + +pub const Color = struct { + r: f32 = 0, + g: f32 = 0, + b: f32 = 0, + a: f32 = 1, + + pub const red: Color = Color.fromVec(.{ 1, 0, 0, 1 }); + + pub fn fromVec(val: @Vector(4, f32)) Color { + return .{ + .r = val[0], + .g = val[1], + .b = val[2], + .a = val[3], + }; + } +}; + +//pub const Program = packed struct(u32) { handle: u32 }; + +const ShaderType = enum { vec3 }; + +const UniformInfo = struct { + identifier: [:0]const u8, + shader_name: []const u8, + kind: enum { i32, f32, vec2, vec3, mat3, mat4 }, + + fn SetType(self: UniformInfo) type { + return switch (self.kind) { + .i32 => i32, + .f32 => f32, + .vec2 => [2]f32, + .vec3 => [3]f32, + .mat3 => Mat3, + .mat4 => Mat4, + }; + } +}; + +const AttributeInfo = struct { + identifier: [:0]const u8, + shader_name: []const u8, + kind: enum { vec2, vec3, vec4 }, + instance_divisor: ?u16 = null, +}; + +pub const Buffer = struct { + handle: u32, + + const Self = @This(); + pub fn create() Self { + return .{ .handle = bindings.createBuffer() }; + } + + pub fn bind(self: Self, target: bindings.BindingPoint) void { + bindings.bindBuffer(target, self.handle); + } + + fn FillDataType(comptime shader_type: ShaderType) type { + return switch (shader_type) { + .vec3 => []const [3]f32, + }; + } + + pub fn bindAndFill( + self: Self, + target: bindings.BindingPoint, + comptime kind: ShaderType, + vals: FillDataType(kind), + usage_pattern: bindings.UsagePattern, + ) void { + bindings.bindBuffer(target, self.handle); + switch (kind) { + .vec3 => { + bindings.bufferData(target, @ptrCast(vals.ptr), vals.len * 3, usage_pattern); + }, + } + } +}; + +pub const VertexArrayObject = struct { + handle: u32, + + const Self = @This(); + pub fn init() Self { + return .{ .handle = bindings.createVertexArray() }; + } + + pub fn bind(self: Self) void { + bindings.bindVertexArray(self.handle); + } +}; + +pub fn Program( + comptime uniforms: []const UniformInfo, + comptime attributes: []const AttributeInfo, +) type { + return struct { + pub const Uniform: type = blk: { + const EnumField = std.builtin.Type.EnumField; + var fields: []const EnumField = &.{}; + for (uniforms, 0..) |u, i| { + const field: EnumField = .{ .name = u.identifier, .value = i }; + fields = &(fields[0..i].* ++ [1]EnumField{field}); + } + + const info: std.builtin.Type.Enum = .{ + .fields = fields, + .decls = &.{}, + .is_exhaustive = false, + .tag_type = u32, + }; + break :blk @Type(.{ .Enum = info }); + }; + + pub const Attribute: type = blk: { + const EnumField = std.builtin.Type.EnumField; + var fields: []const EnumField = &.{}; + for (attributes, 0..) |a, i| { + const field: EnumField = .{ .name = a.identifier, .value = i }; + fields = &(fields[0..i].* ++ [1]EnumField{field}); + } + + const info: std.builtin.Type.Enum = .{ + .fields = fields, + .decls = &.{}, + .is_exhaustive = false, + .tag_type = u32, + }; + break :blk @Type(.{ .Enum = info }); + }; + + handle: u32, + uniform_handles: [uniforms.len]i32, + attribute_handles: [attributes.len]i32, + + const Self = @This(); + pub fn init(vert_src: []const u8, frag_src: []const u8) !Self { + const vert = try loadShader(.vertex, vert_src); + errdefer bindings.deleteShader(vert); + const frag = try loadShader(.fragment, frag_src); + errdefer bindings.deleteShader(frag); + + const handle = try createProgram(vert, frag); + + var uniform_handles: [uniforms.len]i32 = undefined; + for (uniforms, 0..) |u, i| { + const h = bindings.getUniformLocation(handle, u.shader_name.ptr, u.shader_name.len); + if (h < 0) return error.FailedToGetUniformLocation; + uniform_handles[i] = h; + } + + var attribute_handles: [attributes.len]i32 = undefined; + for (attributes, 0..) |a, i| { + const h = bindings.getAttribLocation(handle, a.shader_name.ptr, a.shader_name.len); + if (h < 0) return error.FailedToGetUniformLocation; + + attribute_handles[i] = h; + } + + return .{ + .handle = handle, + .uniform_handles = uniform_handles, + .attribute_handles = attribute_handles, + }; + } + + pub fn use(self: Self) void { + bindings.useProgram(self.handle); + } + + pub fn setUniform( + self: Self, + comptime uniform: Uniform, + val: uniforms[@intFromEnum(uniform)].SetType(), + ) void { + const handle = self.uniform_handles[@intFromEnum(uniform)]; + const info = uniforms[@intFromEnum(uniform)]; + + switch (info.kind) { + .i32 => bindings.uniform1i(handle, val), + .f32 => bindings.uniform1f(handle, val), + .vec2 => bindings.uniform2f(handle, val[0], val[1]), + .vec3 => bindings.uniform3f(handle, val[0], val[1], val[2]), + .mat3 => { + const v: *const [9]f32 = @ptrCast(&val.data); + bindings.uniformMatrix3fv(handle, v); + }, + .mat4 => { + const v: *const [16]f32 = @ptrCast(&val.data); + bindings.uniformMatrix4fv(handle, v); + }, + } + } + + pub fn setVertexAttribPointer( + self: Self, + comptime attribute: Self.Attribute, + vao: VertexArrayObject, + buffer: Buffer, + ) void { + vao.bind(); + buffer.bind(.array_buffer); + const attr_info = attributes[@intFromEnum(attribute)]; + const attr_handle = self.attribute_handles[@intFromEnum(attribute)]; + bindings.enableVertexAttribArray(attr_handle); + + const size = switch (attr_info.kind) { + .vec2 => 2, + .vec3, .vec4 => 3, + }; + const a_type = switch (attr_info.kind) { + .vec2, .vec3, .vec4 => .f32, + }; + const normalize = .false; + const stride = 0; + const offset = 0; + bindings.vertexAttribPointer( + attr_handle, + size, + a_type, + normalize, + stride, + offset, + ); + + if (attr_info.instance_divisor) |d| { + bindings.vertexAttribDivisor(attr_handle, d); + } + } + }; +} + +pub fn clear(col: Color) void { + bindings.clearColor(col.r, col.g, col.b, col.a); + bindings.clear(bindings.color_buffer_bit | bindings.depth_buffer_bit); +} + +pub fn getScreenSize() [2]f32 { + return .{ bindings.getScreenWidth(), bindings.getScreenHeight() }; +} + +pub const viewport = bindings.viewport; +pub fn viewportToScreen() void { + const dims = getScreenSize(); + viewport(0, 0, @intFromFloat(dims[0]), @intFromFloat(dims[1])); +} + +pub const drawArrays = bindings.drawArrays; +pub const drawArraysInstanced = bindings.drawArraysInstanced; + +fn loadShader(shader_type: bindings.ShaderType, source: []const u8) !u32 { + const shader = bindings.createShader(shader_type); + errdefer bindings.deleteShader(shader); + + bindings.shaderSource(shader, source.ptr, source.len); + bindings.compileShader(shader); + + if (bindings.getShaderParameter(shader, .compile_status) == 0) { + var buf: [512]u8 = undefined; + const len = bindings.getShaderInfoLog(shader, &buf, buf.len); + const msg = buf[0..len]; + wasm.print("{s}", .{msg}); + return error.CompilationFailed; + } + + return shader; +} + +fn createProgram(vert: u32, frag: u32) !u32 { + const program = bindings.createProgram(); + errdefer bindings.deleteProgram(program); + + bindings.attachShader(program, vert); + bindings.attachShader(program, frag); + bindings.linkProgram(program); + if (bindings.getProgramParameter(program, .link_status) == 0) { + var buf: [512]u8 = undefined; + const len = bindings.getProgramInfoLog(program, &buf, buf.len); + const msg = buf[0..len]; + wasm.print("{s}", .{msg}); + return error.LinkFailed; + } + + return program; +} + +pub const Capability = enum(u32) { + cull_face = 0x0B44, + depth_test = 0x0B71, +}; + +pub fn enable(cap: Capability) void { + bindings.enable(@intFromEnum(cap)); +} diff --git a/src/webgl_bindings.zig b/src/webgl_bindings.zig new file mode 100644 index 0000000..cb1bf29 --- /dev/null +++ b/src/webgl_bindings.zig @@ -0,0 +1,157 @@ +pub const Texture = packed struct(u32) { handle: u32 }; + +pub const GLProgramInfo = enum(u32) { + delete_status = 0x8B80, + link_status = 0x8B82, +}; + +pub const ShaderInfo = enum(u32) { + shader_type = 0x8B4F, + delete_status = 0x8B80, + compile_status = 0x8B81, +}; + +pub const BindingPoint = enum(u32) { + array_buffer = 0x8892, + element_array_buffer = 0x8893, +}; + +pub const UsagePattern = enum(u32) { + stream_draw = 0x88E0, + static_draw = 0x88E4, + dynamic_draw = 0x88E8, +}; + +pub const color_buffer_bit = 0x00004000; +pub const depth_buffer_bit = 0x00000100; + +pub extern fn getScreenHeight() f32; +pub extern fn getScreenWidth() f32; + +pub extern fn clearColor(r: f32, g: f32, b: f32, a: f32) void; +pub extern fn clear(mask: u32) void; + +pub extern fn enable(cap: u32) void; +pub extern fn blendFunc(sfactor: u32, dfactor: u32) void; + +pub extern fn createBuffer() u32; +pub extern fn bindBuffer(target: BindingPoint, buffer: u32) void; +pub extern fn bufferData( + target: BindingPoint, + ptr: [*]const f32, + len: usize, + usage: UsagePattern, +) void; + +pub const ShaderType = enum(u32) { + fragment = 0x8B30, + vertex = 0x8B31, +}; + +pub extern fn createShader(shader_type: ShaderType) u32; +pub extern fn shaderSource(shader: u32, source_ptr: [*c]const u8, len: usize) void; +pub extern fn compileShader(shader: u32) void; +pub extern fn getShaderParameter(shader: u32, info: ShaderInfo) u32; +pub extern fn deleteShader(shader: u32) void; +pub extern fn getShaderInfoLog(shader: u32, buf: [*c]u8, len: usize) u32; + +pub extern fn createProgram() u32; +pub extern fn deleteProgram(program: u32) void; +pub extern fn attachShader(program: u32, shader: u32) void; +pub extern fn linkProgram(program: u32) void; +pub extern fn getProgramParameter(program: u32, info: GLProgramInfo) u32; +pub extern fn getProgramInfoLog(program: u32, buf: [*c]const u8, len: usize) u32; +pub extern fn useProgram(program: u32) void; + +pub extern fn getAttribLocation(program: u32, buf: [*c]const u8, len: usize) i32; +pub extern fn getUniformLocation(program: u32, buf: [*c]const u8, len: usize) i32; + +pub extern fn uniform1i(location: i32, v0: i32) void; +pub extern fn uniform1f(location: i32, v0: f32) void; +pub extern fn uniform2f(location: i32, v0: f32, v1: f32) void; +pub extern fn uniform3f(location: i32, v0: f32, v1: f32, v2: f32) void; +pub extern fn uniform4f(location: i32, v0: f32, v1: f32, v2: f32, v3: f32) void; +pub extern fn uniformMatrix3fv(location: i32, ptr: *const [9]f32) void; +pub extern fn uniformMatrix4fv(location: i32, ptr: *const [16]f32) void; + +const GLType = enum(u32) { + i8 = 0x1400, + u8 = 0x1401, + i16 = 0x1402, + u16 = 0x1403, + i32 = 0x1404, + u32 = 0x1405, + f32 = 0x1406, +}; + +const GLBool = enum(i32) { + false = 0, + true = 1, +}; + +pub extern fn createVertexArray() u32; +pub extern fn bindVertexArray(vao: u32) void; +pub extern fn enableVertexAttribArray(attrib_location: i32) void; +pub extern fn vertexAttribPointer( + attrib_location: i32, + size: i32, + gl_type: GLType, + normalize: GLBool, + stride: i32, + offset: i32, +) void; + +pub extern fn viewport(x: i32, y: i32, width: i32, height: i32) void; + +const DrawMode = enum(u32) { + points = 0x0000, + lines = 0x0001, + line_loop = 0x0002, + line_strip = 0x0003, + triangles = 0x0004, + triangle_strip = 0x00005, + triangle_fan = 0x00006, +}; + +pub extern fn drawArrays(mode: DrawMode, first: i32, count: i32) void; + +pub extern fn createTexture() Texture; + +const TextureBindTarget = enum(u32) { + texture_2d = 0x0DE1, + texture_cube_map = 0x8514, + texture_3d = 0x806F, + texture_2d_array = 0x8C1A, +}; +pub extern fn bindTexture(target: TextureBindTarget, texture: Texture) void; + +pub const texture_0 = 0x84c0; +pub extern fn activeTexture(idx: u32) void; + +const TextureTarget = enum(u32) { + texture_2d = 0x0DE1, +}; +const TextureFormat = enum(u32) { + rgba = 0x1908, +}; +pub extern fn texImage2D( + target: TextureTarget, + level: i32, + internal_format: TextureFormat, + width: i32, + height: i32, + border: i32, + format: TextureFormat, + data_type: GLType, + ptr: *const anyopaque, + count: u32, + offset: u32, +) void; + +pub extern fn vertexAttribDivisor(loc: i32, divisor: i32) void; +pub extern fn drawArraysInstanced( + mode: DrawMode, + first: i32, + count: i32, + instace_count: i32, +) void; |