summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoralaric <alaric@netmythos.org>2024-04-06 05:35:32 -0700
committeralaric <alaric@netmythos.org>2024-04-06 05:35:32 -0700
commitb614db3cf2a89f565b06471dc47e1bd345c0b070 (patch)
treebbda227d10290bc7c28f68c2768fb3d87d696279
downloadsnake-b614db3cf2a89f565b06471dc47e1bd345c0b070.tar.gz
snake-b614db3cf2a89f565b06471dc47e1bd345c0b070.zip
Made snake.
-rw-r--r--.gitignore1
-rw-r--r--build.zig37
-rw-r--r--build.zig.zon62
-rw-r--r--flake.lock146
-rw-r--r--flake.nix48
-rw-r--r--src/fragment.glsl12
-rw-r--r--src/geometry.zig11
-rw-r--r--src/grid_vertex.glsl22
-rw-r--r--src/matrix.zig141
-rw-r--r--src/shell.html136
-rw-r--r--src/vertex.glsl14
-rw-r--r--src/wasm.zig34
-rw-r--r--src/wasm_snake.zig330
-rw-r--r--src/webgl.js283
-rw-r--r--src/webgl.zig302
-rw-r--r--src/webgl_bindings.zig157
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;