Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DVD cart to showcase #66

Merged
merged 1 commit into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions showcase/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const carts = .{
.{ "metalgear_timer", @import("metalgear_timer") },
.{ "raytracer", @import("raytracer") },
.{ "neopixelpuzzle", @import("neopixelpuzzle") },
.{ "dvd", @import("dvd") },
};

pub fn build(b: *std.Build) void {
Expand Down
1 change: 1 addition & 0 deletions showcase/build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
.metalgear_timer = .{ .path = "carts/metalgear-timer" },
.raytracer = .{ .path = "carts/raytracer" },
.neopixelpuzzle = .{ .path = "carts/neopixelpuzzle" },
.dvd = .{ .path = "carts/dvd" },
},
.paths = .{
"README.md",
Expand Down
Binary file added showcase/carts/dvd/assets/dvd.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 55 additions & 0 deletions showcase/carts/dvd/build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const std = @import("std");
const Build = std.Build;
const sycl_badge = @import("sycl_badge");

pub const author_name = "Stevie Hryciw";
pub const author_handle = "hryx";
pub const cart_title = "dvd";
pub const description = "Bouncing DVD logo screensaver";

pub fn build(b: *Build) void {
const optimize = b.standardOptimizeOption(.{});
const sycl_badge_dep = b.dependency("sycl_badge", .{});

const cart = sycl_badge.add_cart(sycl_badge_dep, b, .{
.name = "dvd",
.optimize = optimize,
.root_source_file = b.path("src/main.zig"),
});
add_dvd_assets_step(b, sycl_badge_dep, cart);
cart.install(b);
}

// Thank you to Fabio for the code generation step.
fn add_dvd_assets_step(
b: *Build,
sycl_badge_dep: *Build.Dependency,
cart: *sycl_badge.Cart,
) void {
const convert = b.addExecutable(.{
.name = "convert_gfx",
.root_source_file = b.path("build/convert_gfx.zig"),
.target = b.host,
.optimize = cart.options.optimize,
.link_libc = true,
});
convert.root_module.addImport("zigimg", b.dependency("zigimg", .{}).module("zigimg"));

const gen_gfx = b.addRunArtifact(convert);
gen_gfx.addArg("-i");
gen_gfx.addFileArg(b.path("assets/dvd.png"));
gen_gfx.addArg(std.fmt.comptimePrint("{}", .{8}));
gen_gfx.addArg(std.fmt.comptimePrint("{}", .{false}));
gen_gfx.addArg("-o");
const gfx_zig = gen_gfx.addOutputFileArg("gfx.zig");

const gfx_mod = b.addModule("gfx", .{
.root_source_file = gfx_zig,
.optimize = cart.options.optimize,
});
gfx_mod.addImport("cart-api", sycl_badge_dep.module("cart-api"));

cart.wasm.step.dependOn(&gen_gfx.step);
cart.wasm.root_module.addImport("gfx", gfx_mod);
cart.cart_lib.root_module.addImport("gfx", gfx_mod);
}
18 changes: 18 additions & 0 deletions showcase/carts/dvd/build.zig.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.{
.name = "dvd",
.version = "0.0.0",
.dependencies = .{
.sycl_badge = .{ .path = "../../.." },
.zigimg = .{
.url = "https://github.com/zigimg/zigimg/archive/637974e2d31dcdbc33f1e9cc8ffb2e46abd2e215.tar.gz",
.hash = "122012026c3a65ff1d4acba3b3fe80785f7cee9c6b4cdaff7ed0fbf23b0a6c803989",
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
"build",
"assets",
},
}
114 changes: 114 additions & 0 deletions showcase/carts/dvd/build/convert_gfx.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
const std = @import("std");
const allocator = std.heap.c_allocator;
const Image = @import("zigimg").Image;

const ConvertFile = struct {
path: []const u8,
bits: u4,
transparency: bool,
};

pub fn main() !void {
var args = try std.process.argsWithAllocator(allocator);
defer args.deinit();

_ = args.next();
var in_files = std.ArrayList(ConvertFile).init(allocator);
var out_path: []const u8 = undefined;
while (args.next()) |arg| {
if (std.mem.eql(u8, arg, "-i")) {
const path = args.next() orelse return error.MissingArg;
const bits = args.next() orelse return error.MissingArg;
const transparency = args.next() orelse return error.MissingArg;
try in_files.append(.{ .path = path, .bits = @intCast(bits[0] - '0'), .transparency = transparency[0] == 't' });
} else if (std.mem.eql(u8, arg, "-o")) {
out_path = args.next() orelse return error.MissingArg;
}
}

const out_file = try std.fs.cwd().createFile(out_path, .{});
defer out_file.close();

const writer = out_file.writer();
try writer.writeAll("const PackedIntSlice = @import(\"std\").packed_int_array.PackedIntSlice;\n");
try writer.writeAll("const DisplayColor = @import(\"cart-api\").DisplayColor;\n\n");

for (in_files.items) |in_file| {
try convert(in_file, writer);
}
}

fn convert(args: ConvertFile, writer: std.fs.File.Writer) !void {
const N = 8 / args.bits;

var image = try Image.fromFilePath(allocator, args.path);
defer image.deinit();

var colors = std.ArrayList(Color).init(allocator);
defer colors.deinit();
if (args.transparency) try colors.append(.{ .r = 31, .g = 0, .b = 31 });
var indices = try std.ArrayList(usize).initCapacity(allocator, image.width * image.height);
defer indices.deinit();
var it = image.iterator();
while (it.next()) |pixel| {
const color = Color{
.r = @intFromFloat(31.0 * pixel.r),
.g = @intFromFloat(63.0 * pixel.g),
.b = @intFromFloat(31.0 * pixel.b),
};
const index = try getIndex(&colors, color);
indices.appendAssumeCapacity(index);
}
var packed_data = try allocator.alloc(u8, indices.items.len / N);
defer allocator.free(packed_data);
for (packed_data, 0..) |_, i| {
packed_data[i] = 0;
for (0..N) |n| {
const shift: u3 = @intCast(n * args.bits);
packed_data[i] |= @intCast(indices.items[N * i + n] << shift);
}
}

{
const name = std.fs.path.stem(args.path);
try writer.print("pub const {s} = struct {{\n", .{name});

try writer.print(" pub const width = {};\n", .{image.width});
try writer.print(" pub const height = {};\n", .{image.height});

try writer.writeAll(" pub const colors = [_]DisplayColor{\n");
for (colors.items) |c| {
try writer.print(" .{{ .r = {}, .g = {}, .b = {} }},\n", .{ c.r, c.g, c.b });
}
try writer.writeAll(" };\n");

try writer.print(" pub const indices = PackedIntSlice(u{}).init(@constCast(data[0..]), data.len * {});\n", .{ args.bits, N });
try writer.writeAll(" const data = [_]u8{\n");
for (packed_data, 0..) |index, i| {
if (i % 32 == 0) try writer.writeAll(" ");
try writer.print("{}, ", .{index});
if ((i + 1) % 32 == 0) try writer.writeAll("\n");
}
try writer.writeAll(" };\n");

try writer.writeAll("};\n\n");
}
}

pub const Color = packed struct(u16) {
b: u5,
g: u6,
r: u5,

fn eql(self: Color, other: Color) bool {
return @as(u16, @bitCast(self)) == @as(u16, @bitCast(other));
}
};

fn getIndex(colors: *std.ArrayList(Color), color: Color) !usize {
for (colors.items, 0..) |c, i| {
if (c.eql(color)) return i;
}
try colors.append(color);
return colors.items.len - 1;
}
147 changes: 147 additions & 0 deletions showcase/carts/dvd/src/main.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
const std = @import("std");
const cart = @import("cart-api");
const gfx = @import("gfx");

export fn start() void {
// Clear garbage bytes from framebuffer at init since the whole screen is not cleared otherwise.
for (cart.framebuffer[0..]) |*pos| {
pos.* = .{ .r = 0, .b = 0, .g = 0 };
}
}

var dvd_hue: f32 = 0;
var dvd_x: isize = 0;
var dvd_y: isize = 0;
var dvd_dx: isize = 2;
var dvd_dy: isize = 1;
var odd_frame = false;

// These things are super bright at full strength.
const neopixel_brightness = 10.0;

export fn update() void {
const color = hsv_to_rgb(.{ .h = dvd_hue, .s = 1, .v = 1 });
drawDvd(gfx.dvd, @intCast(dvd_x), @intCast(dvd_y), color);
dvd_hue += 5;
if (dvd_hue >= 360.0) dvd_hue = 0;

// The DVD logo gets stuck hitting the places without a fractional angle.
// Offset the dx every other frame to make it look a tiny bit more interesting.
odd_frame = !odd_frame;
dvd_x += if (odd_frame) dvd_dx else @divFloor((dvd_dx * 3), 2);
dvd_y += dvd_dy;
if (dvd_x < 0) {
dvd_dx *= -1;
dvd_x = 0;
}
if (dvd_y < 0) {
dvd_dy *= -1;
dvd_y = 0;
}
if (dvd_x >= cart.screen_width - gfx.dvd.width) {
dvd_dx *= -1;
dvd_x = cart.screen_width - gfx.dvd.width;
}
if (dvd_y >= cart.screen_height - gfx.dvd.height) {
dvd_dy *= -1;
dvd_y = cart.screen_height - gfx.dvd.height;
}

// Press A to light up the neopixels.
// This was just so we could create a light show in the theater. :>
const np_color: cart.NeopixelColor = if (cart.controls.a) .{
.g = @intFromFloat(color.g * neopixel_brightness),
.r = @intFromFloat(color.r * neopixel_brightness),
.b = @intFromFloat(color.b * neopixel_brightness),
} else .{ .g = 0, .r = 0, .b = 0 };
cart.neopixels.* = .{np_color} ** 5;
}

pub fn drawDvd(sprite: anytype, pos_x: usize, pos_y: usize, color: Rgb) void {
var y: usize = 0;
while (y < sprite.height) : (y += 1) {
var x: usize = 0;
while (x < sprite.width) : (x += 1) {
const dst_x = pos_x + x;
const dst_y = pos_y + y;
const index = y * sprite.width + x;
const src = sprite.colors[sprite.indices.get(index)];
var dst = &cart.framebuffer[dst_y * cart.screen_width + dst_x];
dst.r = @intFromFloat(@as(f32, @floatFromInt(src.r)) * color.r);
dst.g = @intFromFloat(@as(f32, @floatFromInt(src.g)) * color.g);
dst.b = @intFromFloat(@as(f32, @floatFromInt(src.b)) * color.b);
cart.framebuffer[dst_y * cart.screen_width + dst_x] = dst.*;
}
}
}

const Hsv = struct {
h: f32,
s: f32,
v: f32,
};

const Rgb = struct {
r: f32,
g: f32,
b: f32,
};

fn hsv_to_rgb(in: Hsv) Rgb {
var hh: f32 = undefined;
var p: f32 = undefined;
var q: f32 = undefined;
var t: f32 = undefined;
var ff: f32 = undefined;
var i: i32 = undefined;
var out: Rgb = undefined;

if (in.s <= 0.0) {
out.r = in.v;
out.g = in.v;
out.b = in.v;
return out;
}
hh = in.h;
if (hh >= 360.0) hh = 0.0;
hh /= 60.0;
i = @intFromFloat(hh);
ff = hh - @as(f32, @floatFromInt(i));
p = in.v * (1.0 - in.s);
q = in.v * (1.0 - (in.s * ff));
t = in.v * (1.0 - (in.s * (1.0 - ff)));

switch (i) {
0 => {
out.r = in.v;
out.g = t;
out.b = p;
},
1 => {
out.r = q;
out.g = in.v;
out.b = p;
},
2 => {
out.r = p;
out.g = in.v;
out.b = t;
},
3 => {
out.r = p;
out.g = q;
out.b = in.v;
},
4 => {
out.r = t;
out.g = p;
out.b = in.v;
},
else => {
out.r = in.v;
out.g = p;
out.b = q;
},
}
return out;
}
Loading