// in this example:
//   - comptime generated image data for texture
//   - Blinn-Phong lighting
//   - several pipelines
//
// quit with escape, q or space
// move camera with arrows or wasd

const std = @import("std");
const mach = @import("mach");
const gpu = @import("gpu");
const glfw = @import("glfw");
const zm = @import("zmath");

const Vec = zm.Vec;
const Mat = zm.Mat;
const Quat = zm.Quat;

const App = @This();

queue: gpu.Queue,
cube: Cube,
camera: Camera,
light: Light,
depth: ?Texture,
keys: u8 = 0,

const Dir = struct {
    const up: u8 = 0b0001;
    const down: u8 = 0b0010;
    const left: u8 = 0b0100;
    const right: u8 = 0b1000;
};

pub fn init(app: *App, engine: *mach.Engine) !void {
    try engine.core.setSizeLimits(.{ .width = 20, .height = 20 }, .{ .width = null, .height = null });

    const eye = vec3(5.0, 7.0, 5.0);
    const target = vec3(0.0, 0.0, 0.0);

    const size = try engine.core.getFramebufferSize();
    const aspect_ratio = @intToFloat(f32, size.width) / @intToFloat(f32, size.height);

    app.queue = engine.gpu_driver.device.getQueue();
    app.cube = Cube.init(engine);
    app.light = Light.init(engine);
    app.depth = null;
    app.camera = Camera.init(engine.gpu_driver.device, eye, target, vec3(0.0, 1.0, 0.0), aspect_ratio, 45.0, 0.1, 100.0);
}

pub fn deinit(app: *App, _: *mach.Engine) void {
    app.depth.?.release();
}

pub fn update(app: *App, engine: *mach.Engine) !bool {
    while (engine.core.pollEvent()) |event| {
        switch (event) {
            .key_press => |ev| switch (ev.key) {
                .q, .escape, .space => engine.core.setShouldClose(true),
                .w, .up => {
                    app.keys |= Dir.up;
                },
                .s, .down => {
                    app.keys |= Dir.down;
                },
                .a, .left => {
                    app.keys |= Dir.left;
                },
                .d, .right => {
                    app.keys |= Dir.right;
                },
                else => {},
            },
            .key_release => |ev| switch (ev.key) {
                .w, .up => {
                    app.keys &= ~Dir.up;
                },
                .s, .down => {
                    app.keys &= ~Dir.down;
                },
                .a, .left => {
                    app.keys &= ~Dir.left;
                },
                .d, .right => {
                    app.keys &= ~Dir.right;
                },
                else => {},
            },
        }
    }

    // move camera
    const speed = zm.f32x4s(@floatCast(f32, engine.delta_time * 5));
    const fwd = zm.normalize3(app.camera.target - app.camera.eye);
    const right = zm.normalize3(zm.cross3(fwd, app.camera.up));

    if (app.keys & Dir.up != 0)
        app.camera.eye += fwd * speed;

    if (app.keys & Dir.down != 0)
        app.camera.eye -= fwd * speed;

    if (app.keys & Dir.right != 0) app.camera.eye += right * speed else if (app.keys & Dir.left != 0) app.camera.eye -= right * speed else app.camera.eye += right * (speed * @Vector(4, f32){ 0.5, 0.5, 0.5, 0.5 });

    app.camera.update(app.queue);

    // move light
    const light_speed = @floatCast(f32, engine.delta_time * 2.5);
    app.light.update(app.queue, light_speed);

    const back_buffer_view = engine.gpu_driver.swap_chain.?.getCurrentTextureView();
    defer back_buffer_view.release();

    const encoder = engine.gpu_driver.device.createCommandEncoder(null);
    defer encoder.release();

    const color_attachment = gpu.RenderPassColorAttachment{
        .view = back_buffer_view,
        .clear_value = gpu.Color{ .r = 0.0, .g = 0.0, .b = 0.4, .a = 1.0 },
        .load_op = .clear,
        .store_op = .store,
    };

    const render_pass_descriptor = gpu.RenderPassEncoder.Descriptor{
        .color_attachments = &.{color_attachment},
        .depth_stencil_attachment = &.{
            .view = app.depth.?.view,
            .depth_load_op = .clear,
            .depth_store_op = .store,
            .stencil_load_op = .none,
            .stencil_store_op = .none,
            .depth_clear_value = 1.0,
        },
    };

    const pass = encoder.beginRenderPass(&render_pass_descriptor);
    defer pass.release();

    // brick cubes
    pass.setPipeline(app.cube.pipeline);
    pass.setBindGroup(0, app.camera.bind_group, &.{});
    pass.setBindGroup(1, app.cube.texture.bind_group, &.{});
    pass.setBindGroup(2, app.light.bind_group, &.{});
    pass.setVertexBuffer(0, app.cube.mesh.buffer, 0, app.cube.mesh.size);
    pass.setVertexBuffer(1, app.cube.instance.buffer, 0, app.cube.instance.size);
    pass.draw(4, app.cube.instance.len, 0, 0);
    pass.draw(4, app.cube.instance.len, 4, 0);
    pass.draw(4, app.cube.instance.len, 8, 0);
    pass.draw(4, app.cube.instance.len, 12, 0);
    pass.draw(4, app.cube.instance.len, 16, 0);
    pass.draw(4, app.cube.instance.len, 20, 0);

    // light source
    pass.setPipeline(app.light.pipeline);
    pass.setBindGroup(0, app.camera.bind_group, &.{});
    pass.setBindGroup(1, app.light.bind_group, &.{});
    pass.setVertexBuffer(0, app.cube.mesh.buffer, 0, app.cube.mesh.size);
    pass.draw(4, 1, 0, 0);
    pass.draw(4, 1, 4, 0);
    pass.draw(4, 1, 8, 0);
    pass.draw(4, 1, 12, 0);
    pass.draw(4, 1, 16, 0);
    pass.draw(4, 1, 20, 0);

    pass.end();

    var command = encoder.finish(null);
    defer command.release();

    app.queue.submit(&.{command});
    engine.gpu_driver.swap_chain.?.present();

    return true;
}

pub fn resize(app: *App, engine: *mach.Engine, width: u32, height: u32) !void {
    // If window is resized, recreate depth buffer otherwise we cannot use it.
    if (app.depth != null) {
        app.depth.?.release();
    }
    // It also recreates the sampler, which is a waste, but for an example it's ok
    app.depth = Texture.depth(engine.gpu_driver.device, width, height);
}

const Camera = struct {
    const Self = @This();

    eye: Vec,
    target: Vec,
    up: Vec,
    aspect: f32,
    fovy: f32,
    near: f32,
    far: f32,
    bind_group: gpu.BindGroup,
    buffer: Buffer,

    const Uniform = struct {
        pos: Vec,
        mat: Mat,
    };

    fn init(device: gpu.Device, eye: Vec, target: Vec, up: Vec, aspect: f32, fovy: f32, near: f32, far: f32) Self {
        var self: Self = .{
            .eye = eye,
            .target = target,
            .up = up,
            .aspect = aspect,
            .near = near,
            .far = far,
            .fovy = fovy,
            .buffer = undefined,
            .bind_group = undefined,
        };

        const view = self.buildViewProjMatrix();

        const uniform = .{
            .pos = self.eye,
            .mat = view,
        };

        const buffer = .{
            .buffer = initBuffer(device, .{ .uniform = true }, &@bitCast([20]f32, uniform)),
            .size = @sizeOf(@TypeOf(uniform)),
        };

        const bind_group = device.createBindGroup(&gpu.BindGroup.Descriptor{
            .layout = Self.bindGroupLayout(device),
            .entries = &[_]gpu.BindGroup.Entry{
                gpu.BindGroup.Entry.buffer(0, buffer.buffer, 0, buffer.size),
            },
        });

        self.buffer = buffer;
        self.bind_group = bind_group;

        return self;
    }

    fn update(self: *Self, queue: gpu.Queue) void {
        const mat = self.buildViewProjMatrix();
        const uniform = .{
            .pos = self.eye,
            .mat = mat,
        };

        queue.writeBuffer(self.buffer.buffer, 0, Uniform, &.{uniform});
    }

    inline fn buildViewProjMatrix(s: *const Camera) Mat {
        const view = zm.lookAtRh(s.eye, s.target, s.up);
        const proj = zm.perspectiveFovRh(s.fovy, s.aspect, s.near, s.far);
        return zm.mul(view, proj);
    }

    inline fn bindGroupLayout(device: gpu.Device) gpu.BindGroupLayout {
        const visibility = .{ .vertex = true, .fragment = true };
        return device.createBindGroupLayout(&gpu.BindGroupLayout.Descriptor{
            .entries = &[_]gpu.BindGroupLayout.Entry{
                gpu.BindGroupLayout.Entry.buffer(0, visibility, .uniform, false, 0),
            },
        });
    }
};

const Buffer = struct {
    buffer: gpu.Buffer,
    size: usize,
    len: u32 = 0,
};

const Cube = struct {
    const Self = @This();

    pipeline: gpu.RenderPipeline,
    mesh: Buffer,
    instance: Buffer,
    texture: Texture,

    const IPR = 20; // instances per row
    const SPACING = 2; // spacing between cubes
    const DISPLACEMENT = vec3u(IPR * SPACING / 2, 0, IPR * SPACING / 2);

    fn init(engine: *mach.Engine) Self {
        const device = engine.gpu_driver.device;

        const texture = Brick.texture(device);

        // instance buffer
        var ibuf: [IPR * IPR * 16]f32 = undefined;

        var z: usize = 0;
        while (z < IPR) : (z += 1) {
            var x: usize = 0;
            while (x < IPR) : (x += 1) {
                const pos = vec3u(x * SPACING, 0, z * SPACING) - DISPLACEMENT;
                const rot = blk: {
                    if (pos[0] == 0 and pos[2] == 0) {
                        break :blk zm.quatFromAxisAngle(vec3u(0, 0, 1), 0.0);
                    } else {
                        break :blk zm.quatFromAxisAngle(zm.normalize3(pos), 45.0);
                    }
                };
                const index = z * IPR + x;
                const inst = Instance{
                    .position = pos,
                    .rotation = rot,
                };
                zm.storeMat(ibuf[index * 16 ..], inst.toMat());
            }
        }

        const instance = Buffer{
            .buffer = initBuffer(device, .{ .vertex = true }, &ibuf),
            .len = IPR * IPR,
            .size = @sizeOf(@TypeOf(ibuf)),
        };

        return Self{
            .mesh = mesh(device),
            .texture = texture,
            .instance = instance,
            .pipeline = pipeline(engine),
        };
    }

    fn pipeline(engine: *mach.Engine) gpu.RenderPipeline {
        const device = engine.gpu_driver.device;

        const layout_descriptor = gpu.PipelineLayout.Descriptor{
            .bind_group_layouts = &.{
                Camera.bindGroupLayout(device),
                Texture.bindGroupLayout(device),
                Light.bindGroupLayout(device),
            },
        };

        const layout = device.createPipelineLayout(&layout_descriptor);
        defer layout.release();

        const shader = device.createShaderModule(&.{
            .code = .{ .wgsl = @embedFile("cube.wgsl") },
        });
        defer shader.release();

        const blend = gpu.BlendState{
            .color = .{
                .operation = .add,
                .src_factor = .one,
                .dst_factor = .zero,
            },
            .alpha = .{
                .operation = .add,
                .src_factor = .one,
                .dst_factor = .zero,
            },
        };

        const color_target = gpu.ColorTargetState{
            .format = engine.gpu_driver.swap_chain_format,
            .write_mask = gpu.ColorWriteMask.all,
            .blend = &blend,
        };

        const fragment = gpu.FragmentState{
            .module = shader,
            .entry_point = "fs_main",
            .targets = &.{color_target},
            .constants = null,
        };

        const descriptor = gpu.RenderPipeline.Descriptor{
            .layout = layout,
            .fragment = &fragment,
            .vertex = .{
                .module = shader,
                .entry_point = "vs_main",
                .buffers = &.{
                    Self.vertexBufferLayout(),
                    Self.instanceLayout(),
                },
            },
            .depth_stencil = &.{
                .format = Texture.DEPTH_FORMAT,
                .depth_write_enabled = true,
                .depth_compare = .less,
            },
            .primitive = .{
                .front_face = .ccw,
                .cull_mode = .back,
                // .cull_mode = .none,
                .topology = .triangle_strip,
                .strip_index_format = .none,
            },
        };

        return device.createRenderPipeline(&descriptor);
    }

    fn mesh(device: gpu.Device) Buffer {
        // generated texture has aspect ratio of 1:2
        // `h` reflects that ratio
        // `v` sets how many times texture repeats across surface
        const v = 2;
        const h = v * 2;
        const buf = asFloats(.{
            // z+ face
            0, 0, 1, 0,  0,  1,  0, h,
            1, 0, 1, 0,  0,  1,  v, h,
            0, 1, 1, 0,  0,  1,  0, 0,
            1, 1, 1, 0,  0,  1,  v, 0,
            // z- face
            1, 0, 0, 0,  0,  -1, 0, h,
            0, 0, 0, 0,  0,  -1, v, h,
            1, 1, 0, 0,  0,  -1, 0, 0,
            0, 1, 0, 0,  0,  -1, v, 0,
            // x+ face
            1, 0, 1, 1,  0,  0,  0, h,
            1, 0, 0, 1,  0,  0,  v, h,
            1, 1, 1, 1,  0,  0,  0, 0,
            1, 1, 0, 1,  0,  0,  v, 0,
            // x- face
            0, 0, 0, -1, 0,  0,  0, h,
            0, 0, 1, -1, 0,  0,  v, h,
            0, 1, 0, -1, 0,  0,  0, 0,
            0, 1, 1, -1, 0,  0,  v, 0,
            // y+ face
            1, 1, 0, 0,  1,  0,  0, h,
            0, 1, 0, 0,  1,  0,  v, h,
            1, 1, 1, 0,  1,  0,  0, 0,
            0, 1, 1, 0,  1,  0,  v, 0,
            // y- face
            0, 0, 0, 0,  -1, 0,  0, h,
            1, 0, 0, 0,  -1, 0,  v, h,
            0, 0, 1, 0,  -1, 0,  0, 0,
            1, 0, 1, 0,  -1, 0,  v, 0,
        });

        return Buffer{
            .buffer = initBuffer(device, .{ .vertex = true }, &buf),
            .size = @sizeOf(@TypeOf(buf)),
        };
    }

    fn vertexBufferLayout() gpu.VertexBufferLayout {
        const attributes = [_]gpu.VertexAttribute{
            .{
                .format = .float32x3,
                .offset = 0,
                .shader_location = 0,
            },
            .{
                .format = .float32x3,
                .offset = @sizeOf([3]f32),
                .shader_location = 1,
            },
            .{
                .format = .float32x2,
                .offset = @sizeOf([6]f32),
                .shader_location = 2,
            },
        };
        return gpu.VertexBufferLayout{
            .array_stride = @sizeOf([8]f32),
            .step_mode = .vertex,
            .attribute_count = attributes.len,
            .attributes = &attributes,
        };
    }

    fn instanceLayout() gpu.VertexBufferLayout {
        const attributes = [_]gpu.VertexAttribute{
            .{
                .format = .float32x4,
                .offset = 0,
                .shader_location = 3,
            },
            .{
                .format = .float32x4,
                .offset = @sizeOf([4]f32),
                .shader_location = 4,
            },
            .{
                .format = .float32x4,
                .offset = @sizeOf([8]f32),
                .shader_location = 5,
            },
            .{
                .format = .float32x4,
                .offset = @sizeOf([12]f32),
                .shader_location = 6,
            },
        };

        return gpu.VertexBufferLayout{
            .array_stride = @sizeOf([16]f32),
            .step_mode = .instance,
            .attribute_count = attributes.len,
            .attributes = &attributes,
        };
    }
};

fn asFloats(comptime arr: anytype) [arr.len]f32 {
    comptime var len = arr.len;
    comptime var out: [len]f32 = undefined;
    comptime var i = 0;
    inline while (i < len) : (i += 1) {
        out[i] = @intToFloat(f32, arr[i]);
    }
    return out;
}

const Brick = struct {
    const W = 12;
    const H = 6;

    fn texture(device: gpu.Device) Texture {
        const slice: []const u8 = &data();
        return Texture.fromData(device, W, H, u8, slice);
    }

    fn data() [W * H * 4]u8 {
        comptime var out: [W * H * 4]u8 = undefined;

        // fill all the texture with brick color
        comptime var i = 0;
        inline while (i < H) : (i += 1) {
            comptime var j = 0;
            inline while (j < W * 4) : (j += 4) {
                out[i * W * 4 + j + 0] = 210;
                out[i * W * 4 + j + 1] = 30;
                out[i * W * 4 + j + 2] = 30;
                out[i * W * 4 + j + 3] = 0;
            }
        }

        const f = 10;

        // fill the cement lines
        inline for ([_]comptime_int{ 0, 1 }) |k| {
            inline for ([_]comptime_int{ 5 * 4, 11 * 4 }) |m| {
                out[k * W * 4 + m + 0] = f;
                out[k * W * 4 + m + 1] = f;
                out[k * W * 4 + m + 2] = f;
                out[k * W * 4 + m + 3] = 0;
            }
        }

        inline for ([_]comptime_int{ 3, 4 }) |k| {
            inline for ([_]comptime_int{ 2 * 4, 8 * 4 }) |m| {
                out[k * W * 4 + m + 0] = f;
                out[k * W * 4 + m + 1] = f;
                out[k * W * 4 + m + 2] = f;
                out[k * W * 4 + m + 3] = 0;
            }
        }

        inline for ([_]comptime_int{ 2, 5 }) |k| {
            comptime var m = 0;
            inline while (m < W * 4) : (m += 4) {
                out[k * W * 4 + m + 0] = f;
                out[k * W * 4 + m + 1] = f;
                out[k * W * 4 + m + 2] = f;
                out[k * W * 4 + m + 3] = 0;
            }
        }

        return out;
    }
};

// don't confuse with gpu.Texture
const Texture = struct {
    const Self = @This();

    texture: gpu.Texture,
    view: gpu.TextureView,
    sampler: gpu.Sampler,
    bind_group: gpu.BindGroup,

    const DEPTH_FORMAT = .depth32_float;
    const FORMAT = .rgba8_unorm;

    fn release(self: *Self) void {
        self.texture.release();
        self.view.release();
        self.sampler.release();
    }

    fn fromData(device: gpu.Device, width: u32, height: u32, comptime T: type, data: []const T) Self {
        const extent = gpu.Extent3D{
            .width = width,
            .height = height,
        };

        const texture = device.createTexture(&gpu.Texture.Descriptor{
            .size = extent,
            .mip_level_count = 1,
            .sample_count = 1,
            .dimension = .dimension_2d,
            .format = FORMAT,
            .usage = .{ .copy_dst = true, .texture_binding = true },
        });

        const view = texture.createView(&gpu.TextureView.Descriptor{
            .aspect = .all,
            .format = FORMAT,
            .dimension = .dimension_2d,
            .base_array_layer = 0,
            .array_layer_count = 1,
            .mip_level_count = 1,
            .base_mip_level = 0,
        });

        const sampler = device.createSampler(&gpu.Sampler.Descriptor{
            .address_mode_u = .repeat,
            .address_mode_v = .repeat,
            .address_mode_w = .repeat,
            .mag_filter = .linear,
            .min_filter = .linear,
            .mipmap_filter = .linear,
            .compare = .none,
            .lod_min_clamp = 0.0,
            .lod_max_clamp = std.math.f32_max,
            .max_anisotropy = 1, // 1,2,4,8,16
        });

        device.getQueue().writeTexture(
            &gpu.ImageCopyTexture{
                .texture = texture,
            },
            &gpu.Texture.DataLayout{
                .bytes_per_row = 4 * width,
                .rows_per_image = height,
            },
            &extent,
            T,
            data,
        );

        const bind_group_layout = Self.bindGroupLayout(device);
        const bind_group = device.createBindGroup(&gpu.BindGroup.Descriptor{
            .layout = bind_group_layout,
            .entries = &[_]gpu.BindGroup.Entry{
                gpu.BindGroup.Entry.textureView(0, view),
                gpu.BindGroup.Entry.sampler(1, sampler),
            },
        });

        return Self{
            .view = view,
            .texture = texture,
            .sampler = sampler,
            .bind_group = bind_group,
        };
    }

    fn depth(device: gpu.Device, width: u32, height: u32) Self {
        const extent = gpu.Extent3D{
            .width = width,
            .height = height,
        };

        const texture = device.createTexture(&gpu.Texture.Descriptor{
            .size = extent,
            .mip_level_count = 1,
            .sample_count = 1,
            .dimension = .dimension_2d,
            .format = DEPTH_FORMAT,
            .usage = .{
                .render_attachment = true,
                .texture_binding = true,
            },
        });

        const view = texture.createView(&gpu.TextureView.Descriptor{
            .aspect = .all,
            .format = .none,
            .dimension = .dimension_2d,
            .base_array_layer = 0,
            .array_layer_count = 1,
            .mip_level_count = 1,
            .base_mip_level = 0,
        });

        const sampler = device.createSampler(&gpu.Sampler.Descriptor{
            .address_mode_u = .clamp_to_edge,
            .address_mode_v = .clamp_to_edge,
            .address_mode_w = .clamp_to_edge,
            .mag_filter = .linear,
            .min_filter = .nearest,
            .mipmap_filter = .nearest,
            .compare = .less_equal,
            .lod_min_clamp = 0.0,
            .lod_max_clamp = std.math.f32_max,
            .max_anisotropy = 1,
        });

        return Self{
            .texture = texture,
            .view = view,
            .sampler = sampler,
            .bind_group = undefined, // not used
        };
    }

    inline fn bindGroupLayout(device: gpu.Device) gpu.BindGroupLayout {
        const visibility = .{ .fragment = true };
        const Entry = gpu.BindGroupLayout.Entry;
        return device.createBindGroupLayout(&gpu.BindGroupLayout.Descriptor{
            .entries = &[_]Entry{
                Entry.texture(0, visibility, .float, .dimension_2d, false),
                Entry.sampler(1, visibility, .filtering),
            },
        });
    }
};

const Light = struct {
    const Self = @This();

    uniform: Uniform,
    buffer: Buffer,
    bind_group: gpu.BindGroup,
    pipeline: gpu.RenderPipeline,

    const Uniform = struct {
        position: Vec,
        color: Vec,
    };

    fn init(engine: *mach.Engine) Self {
        const device = engine.gpu_driver.device;
        const uniform = .{
            .color = vec3u(1, 1, 1),
            .position = vec3u(3, 7, 2),
        };

        const buffer = .{
            .buffer = initBuffer(device, .{ .uniform = true }, &@bitCast([8]f32, uniform)),
            .size = @sizeOf(@TypeOf(uniform)),
        };

        const bind_group = device.createBindGroup(&gpu.BindGroup.Descriptor{
            .layout = Self.bindGroupLayout(device),
            .entries = &[_]gpu.BindGroup.Entry{
                gpu.BindGroup.Entry.buffer(0, buffer.buffer, 0, buffer.size),
            },
        });

        return Self{
            .buffer = buffer,
            .uniform = uniform,
            .bind_group = bind_group,
            .pipeline = Self.pipeline(engine),
        };
    }

    fn update(self: *Self, queue: gpu.Queue, delta: f32) void {
        const old = self.uniform;
        const new = Light.Uniform{
            .position = zm.qmul(zm.quatFromAxisAngle(vec3u(0, 1, 0), delta), old.position),
            .color = old.color,
        };
        queue.writeBuffer(self.buffer.buffer, 0, Light.Uniform, &.{new});
        self.uniform = new;
    }

    inline fn bindGroupLayout(device: gpu.Device) gpu.BindGroupLayout {
        const visibility = .{ .vertex = true, .fragment = true };
        const Entry = gpu.BindGroupLayout.Entry;
        return device.createBindGroupLayout(&gpu.BindGroupLayout.Descriptor{
            .entries = &[_]Entry{
                Entry.buffer(0, visibility, .uniform, false, 0),
            },
        });
    }

    fn pipeline(engine: *mach.Engine) gpu.RenderPipeline {
        const device = engine.gpu_driver.device;

        const layout_descriptor = gpu.PipelineLayout.Descriptor{
            .bind_group_layouts = &.{
                Camera.bindGroupLayout(device),
                Light.bindGroupLayout(device),
            },
        };

        const layout = device.createPipelineLayout(&layout_descriptor);
        defer layout.release();

        const shader = device.createShaderModule(&.{
            .code = .{ .wgsl = @embedFile("light.wgsl") },
        });
        defer shader.release();

        const blend = gpu.BlendState{
            .color = .{
                .operation = .add,
                .src_factor = .one,
                .dst_factor = .zero,
            },
            .alpha = .{
                .operation = .add,
                .src_factor = .one,
                .dst_factor = .zero,
            },
        };

        const color_target = gpu.ColorTargetState{
            .format = engine.gpu_driver.swap_chain_format,
            .write_mask = gpu.ColorWriteMask.all,
            .blend = &blend,
        };

        const fragment = gpu.FragmentState{
            .module = shader,
            .entry_point = "fs_main",
            .targets = &.{color_target},
            .constants = null,
        };

        const descriptor = gpu.RenderPipeline.Descriptor{
            .layout = layout,
            .fragment = &fragment,
            .vertex = .{
                .module = shader,
                .entry_point = "vs_main",
                .buffers = &.{
                    Cube.vertexBufferLayout(),
                },
            },
            .depth_stencil = &.{
                .format = Texture.DEPTH_FORMAT,
                .depth_write_enabled = true,
                .depth_compare = .less,
            },
            .primitive = .{
                .front_face = .ccw,
                .cull_mode = .back,
                // .cull_mode = .none,
                .topology = .triangle_strip,
                .strip_index_format = .none,
            },
        };

        return device.createRenderPipeline(&descriptor);
    }
};

inline fn initBuffer(device: gpu.Device, usage: gpu.BufferUsage, data: anytype) gpu.Buffer {
    std.debug.assert(@typeInfo(@TypeOf(data)) == .Pointer);
    const T = std.meta.Elem(@TypeOf(data));

    var u = usage;
    u.copy_dst = true;
    const buffer = device.createBuffer(&.{
        .size = @sizeOf(T) * data.len,
        .usage = u,
        .mapped_at_creation = true,
    });

    var mapped = buffer.getMappedRange(T, 0, data.len);
    std.mem.copy(T, mapped, data);
    buffer.unmap();
    return buffer;
}

fn vec3i(x: isize, y: isize, z: isize) Vec {
    return zm.f32x4(@intToFloat(f32, x), @intToFloat(f32, y), @intToFloat(f32, z), 0.0);
}

fn vec3u(x: usize, y: usize, z: usize) Vec {
    return zm.f32x4(@intToFloat(f32, x), @intToFloat(f32, y), @intToFloat(f32, z), 0.0);
}

fn vec3(x: f32, y: f32, z: f32) Vec {
    return zm.f32x4(x, y, z, 0.0);
}

fn vec4(x: f32, y: f32, z: f32, w: f32) Vec {
    return zm.f32x4(x, y, z, w);
}

// todo indside Cube
const Instance = struct {
    const Self = @This();

    position: Vec,
    rotation: Quat,

    fn toMat(self: *const Self) Mat {
        return zm.mul(zm.quatToMat(self.rotation), zm.translationV(self.position));
    }
};
