diff --git a/.github/workflows/zig.yml b/.github/workflows/zig.yml new file mode 100644 index 0000000..d6e6d09 --- /dev/null +++ b/.github/workflows/zig.yml @@ -0,0 +1,95 @@ +name: Zig + +on: + push: + branches: [ "main" ] + pull_request: + +jobs: + build: + name: ${{ matrix.name }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + name: Linux x86_64 + zig_archive: zig-x86_64-linux-musl-baseline.tar.xz + zig_dir: zig-x86_64-linux-musl-baseline + - os: macos-latest + name: macOS aarch64 + zig_archive: zig-aarch64-macos-baseline.tar.xz + zig_dir: zig-aarch64-macos-baseline + - os: ubuntu-24.04-arm + name: Linux aarch64 + zig_archive: zig-aarch64-linux-musl-baseline.tar.xz + zig_dir: zig-aarch64-linux-musl-baseline + - os: windows-latest + name: Windows x86_64 + zig_archive: zig-x86_64-windows-baseline.zip + zig_dir: zig-x86_64-windows-baseline + runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@main + + - name: Cache zig-mos toolchain + id: cache-zig + uses: actions/cache@main + with: + path: ${{ matrix.zig_dir }} + key: zig-mos-${{ matrix.zig_archive }}-v1 + + - name: Download zig-mos toolchain + if: steps.cache-zig.outputs.cache-hit != 'true' + env: + ZIG_ARCHIVE: ${{ matrix.zig_archive }} + ZIG_DIR: ${{ matrix.zig_dir }} + run: | + curl -fsSL -O "https://github.com/kassane/zig-mos-bootstrap/releases/download/0.17.0/${ZIG_ARCHIVE}" + if [[ "${ZIG_ARCHIVE}" == *.zip ]]; then unzip -q "${ZIG_ARCHIVE}"; else tar -xf "${ZIG_ARCHIVE}"; fi + + - name: Add zig-mos to PATH + env: + ZIG_DIR: ${{ matrix.zig_dir }} + run: echo "$PWD/${ZIG_DIR}" >> "$GITHUB_PATH" + + - name: Cache zig packages + uses: actions/cache@main + with: + path: | + zig/hello/zig-pkg + zig/plasma/zig-pkg + key: zig-pkg-${{ hashFiles('zig/**/build.zig.zon') }} + + - name: Build zig/hello + working-directory: zig/hello + run: | + zig build --summary all + zig build -Doptimize=ReleaseSmall --summary all + + - name: Build zig/plasma + working-directory: zig/plasma + run: | + zig build --summary all + zig build -Doptimize=ReleaseSmall --summary all + + - name: Verify outputs + run: | + ls -la zig/hello/zig-out/bin/ zig/plasma/zig-out/bin/ + for f in zig/hello/zig-out/bin/hello.prg zig/plasma/zig-out/bin/plasma.prg; do + if ! test -s "$f"; then echo "MISSING: $f"; exit 1; fi + echo "OK: $f ($(wc -c < "$f") bytes)" + done + + - name: Upload built binaries + if: matrix.os == 'ubuntu-latest' + uses: actions/upload-artifact@main + with: + name: zig-prgs + path: | + zig/hello/zig-out/bin/ + zig/plasma/zig-out/bin/ + retention-days: 1 diff --git a/.gitignore b/.gitignore index b6bfa50..3c0b418 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,6 @@ cpm-package-lock.cmake # rust target/ Cargo.lock + +# zig +*zig-*/ \ No newline at end of file diff --git a/zig/hello/README.md b/zig/hello/README.md new file mode 100644 index 0000000..787a184 --- /dev/null +++ b/zig/hello/README.md @@ -0,0 +1,45 @@ +# Hello world in Zig (`zig build`) + +This is a minimal Zig program for the MEGA65: a greeting written straight into +screen RAM at `$0800`, and the border set to green. There is no runtime and no +libc, so `main` is a handful of stores. The ASCII to screen-code conversion +happens at comptime. + +Zig compiles the startup assembly, the linker-script glue and a few libc +sources itself for `-target mos-mega65`. They come in as the `llvm-mos-sdk` +package, so no separate llvm-mos toolchain installation is needed. + +## Building and Running + +1. Install [zig-mos](https://github.com/kassane/zig-mos-bootstrap/releases), + the `mos` fork of Zig. +2. Build: + ~~~ bash + cd zig/hello + zig build # writes zig-out/bin/hello.prg + zig build -Doptimize=ReleaseSmall # 229 bytes instead of 821 + ~~~ + +Run in Xemu and dump the screen: + +~~~ bash +timeout 30 xemu-xmega65 -headless -testing -besure \ + -sdimg ~/.local/share/xemu-lgb/mega65/mega65.img \ + -prg zig-out/bin/hello.prg -screenshot hello.png -dumpscreen hello.txt +~~~ + +The greeting appears on the line below the `RUN:` prompt. + +## Metadata + +Field | Value +------------- | --------------------------------------------- +title | Hello world +description | A greeting written directly to screen RAM +author | The MEGA65 Community +keywords | hello, screen, zig +languages | zig +requirements | zig-mos +rom | 920376 +prgs | hello.prg +build | `zig build` diff --git a/zig/hello/build.zig b/zig/hello/build.zig new file mode 100644 index 0000000..5d9cd5f --- /dev/null +++ b/zig/hello/build.zig @@ -0,0 +1,100 @@ +//! MEGA65 hello build. Zig compiles the startup assembly, the linker-script +//! glue and a few libc sources itself for `-target mos-mega65`, so no separate +//! llvm-mos toolchain install is needed. +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{ + .default_target = .{ + .cpu_arch = .mos, + .os_tag = .mega65, + }, + }); + const optimize = b.standardOptimizeOption(.{}); + + const sdk = b.dependency("llvm-mos-sdk", .{}); + const sdk_root = sdk.builder.root.root_dir.path orelse "."; + + const common = b.fmt("{s}/mos-platform/common", .{sdk_root}); + const crt0_dir = b.fmt("{s}/crt0", .{common}); + const com_inc = b.fmt("{s}/include", .{common}); + const com_asm = b.fmt("{s}/asminc", .{common}); + const plat_dir = b.fmt("{s}/mos-platform/mega65", .{sdk_root}); + const comm_dir = b.fmt("{s}/mos-platform/commodore", .{sdk_root}); + + const exe = b.addExecutable(.{ + .name = "hello", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/hello.zig"), + .target = target, + .optimize = optimize, + .sanitize_c = .off, + // MOS has no stack-guard lowering, so the SSP pass fails on any + // function with a sized local buffer. + .stack_protector = false, + }), + }); + exe.bundle_compiler_rt = false; + exe.lto = .full; + + exe.root_module.addIncludePath(.{ .cwd_relative = com_inc }); + exe.root_module.addIncludePath(.{ .cwd_relative = com_asm }); + + // crt0.S and save-basic.S only contribute section contents and export no + // symbol, so an archive member would be dropped. Link them directly. + exe.root_module.addObject(sectionOnlyObj(b, target, optimize, "crt0", crt0_dir, com_asm, com_inc, &.{"crt0.S"})); + exe.root_module.addObject(sectionOnlyObj(b, target, optimize, "save-basic", comm_dir, com_asm, com_inc, &.{"save-basic.S"})); + + exe.root_module.addCSourceFiles(.{ + .root = .{ .cwd_relative = crt0_dir }, + .files = &.{ "init-stack.S", "copy-zp-data.c" }, + }); + exe.root_module.addCSourceFiles(.{ + .root = .{ .cwd_relative = b.fmt("{s}/exit", .{crt0_dir}) }, + .files = &.{ "exit-custom.S", "exit-loop.c", "exit.c" }, + }); + exe.root_module.addCSourceFiles(.{ + .root = .{ .cwd_relative = plat_dir }, + .files = &.{ "basic-header.S", "unmap-basic.S" }, + }); + exe.root_module.addCSourceFiles(.{ .root = .{ .cwd_relative = b.fmt("{s}/c", .{common}) }, .files = &.{"mem.c"} }); + + exe.forceUndefinedSymbol("main"); + exe.setLinkerScript(wrapperLd(b, sdk_root)); + + b.getInstallStep().dependOn(&b.addInstallArtifact(exe, .{ .dest_sub_path = "hello.prg" }).step); +} + +fn sectionOnlyObj(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, name: []const u8, root: []const u8, com_asm: []const u8, com_inc: []const u8, files: []const []const u8) *std.Build.Step.Compile { + const obj = b.addObject(.{ + .name = name, + .root_module = b.createModule(.{ .target = target, .optimize = optimize }), + }); + obj.root_module.addIncludePath(.{ .cwd_relative = com_asm }); + obj.root_module.addIncludePath(.{ .cwd_relative = com_inc }); + obj.root_module.addCSourceFiles(.{ .root = .{ .cwd_relative = root }, .files = files }); + obj.lto = .none; + return obj; +} + +/// mega65/link.ld merged with commodore/commodore.ld, with the INPUT() +/// directives replaced by the objects linked above. +fn wrapperLd(b: *std.Build, sdk_root: []const u8) std.Build.LazyPath { + return b.addWriteFiles().add("mega65-wrapper.ld", b.fmt( + \\SEARCH_DIR("{0s}/mos-platform/mega65"); + \\SEARCH_DIR("{0s}/mos-platform/commodore"); + \\SEARCH_DIR("{0s}/mos-platform/common/ldscripts"); + \\__basic_zp_start = 0x0002; + \\__basic_zp_end = 0x0090; + \\MEMORY {{ ram (rw) : ORIGIN = 0x2001, LENGTH = 0xafff }} + \\__rc0 = __basic_zp_start; + \\INCLUDE "imag-regs.ld" + \\__basic_zp_size = __basic_zp_end - __basic_zp_start; + \\MEMORY {{ zp : ORIGIN = __rc31 + 1, LENGTH = __basic_zp_end - (__rc31 + 1) }} + \\REGION_ALIAS("c_readonly", ram) + \\REGION_ALIAS("c_writeable", ram) + \\SECTIONS {{ .basic_header : {{ *(.basic_header) }} INCLUDE "c.ld" }} + \\__stack = 0xd000; + \\OUTPUT_FORMAT {{ SHORT(0x2001) TRIM(ram) }} + , .{sdk_root})); +} diff --git a/zig/hello/build.zig.zon b/zig/hello/build.zig.zon new file mode 100644 index 0000000..c0e57d3 --- /dev/null +++ b/zig/hello/build.zig.zon @@ -0,0 +1,14 @@ +.{ + .name = .hello, + .version = "0.0.1", + .minimum_zig_version = "0.17.0-mos", + .paths = .{""}, + .fingerprint = 0x3610a6867e5ac473, + .dependencies = .{ + // Startup assembly, linker scripts and libc sources. + .@"llvm-mos-sdk" = .{ + .url = "git+https://github.com/llvm-mos/llvm-mos-sdk#3f6968bbc156ff9a63102a8e158db868819bd61c", + .hash = "N-V-__8AAKSeLAA0M14Y-qtR7Srg-0spUC9lk4H3234zgjc1", + }, + }, +} diff --git a/zig/hello/info.json b/zig/hello/info.json new file mode 100644 index 0000000..197605f --- /dev/null +++ b/zig/hello/info.json @@ -0,0 +1,21 @@ +{ + "title": "Hello world,", + "description": "A greeting written directly to screen RAM from Zig", + "author": "The MEGA65 Community", + "keywords": [ + "hello", + "screen", + "zig" + ], + "languages": [ + "zig" + ], + "requirements": [ + "zig-mos" + ], + "rom": 920376, + "prgs": [ + "hello.prg" + ], + "build": "zig build" +} diff --git a/zig/hello/src/hello.zig b/zig/hello/src/hello.zig new file mode 100644 index 0000000..f1413f4 --- /dev/null +++ b/zig/hello/src/hello.zig @@ -0,0 +1,18 @@ +//! MEGA65 hello: writes a greeting to screen RAM and sets the border colour. +const std = @import("std"); + +/// Uppercase A-Z become screen codes 1-26; everything else maps to itself. +fn screenCodes(comptime s: []const u8) [s.len]u8 { + var out: [s.len]u8 = undefined; + for (s, 0..) |c, i| + out[i] = if (std.ascii.isUpper(c)) c - 'A' + 1 else c; + return out; +} + +export fn main() void { + // 80-column screen RAM at $0800; row 15 sits just below the RUN: prompt. + const screen: [*]volatile u8 = @ptrFromInt(0x0800); + const msg = comptime screenCodes("HELLO, ZIG! WELCOME TO MEGA65."); + for (msg, 0..) |code, i| screen[14 * 80 + i] = code; + @as(*volatile u8, @ptrFromInt(0xd020)).* = 5; // green border +} diff --git a/zig/hello/zig-out/bin/hello.prg b/zig/hello/zig-out/bin/hello.prg new file mode 100644 index 0000000..a37eda4 Binary files /dev/null and b/zig/hello/zig-out/bin/hello.prg differ diff --git a/zig/plasma/README.md b/zig/plasma/README.md new file mode 100644 index 0000000..e943410 --- /dev/null +++ b/zig/plasma/README.md @@ -0,0 +1,46 @@ +# Plasma effect in Zig (`zig build`) + +This is a port of [`c/plasma`](../../c/plasma) to Zig. It keeps the same sine +table, screen layout and drawing loop, so the pattern on screen matches the C +build. + +Unlike `zig/hello`, this one calls into C. The mega65-libc headers `conio.h` +and `random.h` are translated into a Zig module with `b.addTranslateC`, so +`plasma.zig` can call `setcharsetaddr` and `rand8` directly. The `conio.c` and +`random.c` behind those declarations are compiled by Zig from the same package. + +## Building and Running + +1. Install [zig-mos](https://github.com/kassane/zig-mos-bootstrap/releases), + the `mos` fork of Zig. +2. Build: + ~~~ bash + cd zig/plasma + zig build # writes zig-out/bin/plasma.prg + zig build -Doptimize=ReleaseSmall + ~~~ + +Run in Xemu: + +~~~ bash +timeout 30 xemu-xmega65 -headless -testing -besure \ + -sdimg ~/.local/share/xemu-lgb/mega65/mega65.img \ + -prg zig-out/bin/plasma.prg -screenshot plasma.png +~~~ + +The whole 80x25 screen fills with a moving pattern and the program keeps +running, so Xemu needs a timeout. + +## Metadata + +Field | Value +------------- | --------------------------------------------- +title | Plasma effect in Zig +description | A demo-inspired plasma effect in zig +author | The MEGA65 Community +keywords | plasma, charset, zig +languages | zig +requirements | zig-mos +rom | 920376 +prgs | plasma.prg +build | `zig build` diff --git a/zig/plasma/build.zig b/zig/plasma/build.zig new file mode 100644 index 0000000..d055fc3 --- /dev/null +++ b/zig/plasma/build.zig @@ -0,0 +1,127 @@ +//! MEGA65 plasma build. Same zig-only startup as zig/hello, plus the +//! mega65-libc C headers translated into a Zig module for `plasma.zig`. +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{ + .default_target = .{ + .cpu_arch = .mos, + .os_tag = .mega65, + }, + }); + const optimize = b.standardOptimizeOption(.{}); + + const sdk = b.dependency("llvm-mos-sdk", .{}); + const sdk_root = sdk.builder.root.root_dir.path orelse "."; + const libc = b.dependency("mega65-libc", .{}); + + const common = b.fmt("{s}/mos-platform/common", .{sdk_root}); + const crt0_dir = b.fmt("{s}/crt0", .{common}); + const com_inc = b.fmt("{s}/include", .{common}); + const com_asm = b.fmt("{s}/asminc", .{common}); + const plat_dir = b.fmt("{s}/mos-platform/mega65", .{sdk_root}); + const comm_dir = b.fmt("{s}/mos-platform/commodore", .{sdk_root}); + + const exe = b.addExecutable(.{ + .name = "plasma", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/plasma.zig"), + .target = target, + .optimize = optimize, + .sanitize_c = .off, + // LLVM-MOS has no stack-guard lowering, so the SSP pass fails on any + // function with a sized local buffer. + .stack_protector = false, + .imports = &.{.{ + .name = "mega65", + .module = mega65Headers(b, sdk, libc, target, optimize), + }}, + }), + }); + exe.bundle_compiler_rt = false; + exe.lto = .full; + + exe.root_module.addIncludePath(.{ .cwd_relative = com_inc }); + exe.root_module.addIncludePath(.{ .cwd_relative = com_asm }); + exe.root_module.addIncludePath(libc.path("include")); + exe.root_module.addIncludePath(libc.path("include/mega65")); + + // crt0.S and save-basic.S only contribute section contents and export no + // symbol, so an archive member would be dropped. Link them directly. + exe.root_module.addObject(sectionOnlyObj(b, target, optimize, "crt0", crt0_dir, com_asm, com_inc, &.{"crt0.S"})); + exe.root_module.addObject(sectionOnlyObj(b, target, optimize, "save-basic", comm_dir, com_asm, com_inc, &.{"save-basic.S"})); + + exe.root_module.addCSourceFiles(.{ + .root = .{ .cwd_relative = crt0_dir }, + .files = &.{ "init-stack.S", "copy-zp-data.c" }, + }); + exe.root_module.addCSourceFiles(.{ + .root = .{ .cwd_relative = b.fmt("{s}/exit", .{crt0_dir}) }, + .files = &.{ "exit-custom.S", "exit-loop.c", "exit.c" }, + }); + exe.root_module.addCSourceFiles(.{ + .root = .{ .cwd_relative = plat_dir }, + .files = &.{ "basic-header.S", "unmap-basic.S" }, + }); + exe.root_module.addCSourceFiles(.{ .root = .{ .cwd_relative = b.fmt("{s}/c", .{common}) }, .files = &.{"mem.c"} }); + // mega65-libc implementations behind the translated headers. + exe.root_module.addCSourceFiles(.{ .root = libc.path("src"), .files = &.{ "conio.c", "random.c" } }); + exe.root_module.addCSourceFiles(.{ .root = .{ .cwd_relative = comm_dir }, .files = &.{"abort.c"} }); + + exe.forceUndefinedSymbol("main"); + exe.setLinkerScript(wrapperLd(b, sdk_root)); + + b.getInstallStep().dependOn(&b.addInstallArtifact(exe, .{ .dest_sub_path = "plasma.prg" }).step); +} + +fn mega65Headers(b: *std.Build, sdk: *std.Build.Dependency, libc: *std.Build.Dependency, target: std.Build.ResolvedTarget, opt: std.builtin.OptimizeMode) *std.Build.Module { + const tc = b.addTranslateC(.{ + .root_source_file = b.addWriteFiles().add("mega65-api.h", + \\#include + \\#include + ), + .target = target, + .optimize = opt, + .link_libc = false, + }); + tc.defineCMacro("__STDC_HOSTED__", "0"); + tc.addIncludePath(libc.path("include")); + tc.addIncludePath(libc.path("include/mega65")); + tc.addIncludePath(sdk.path("mos-platform/common/include")); + tc.addIncludePath(sdk.path("mos-platform/commodore")); + return tc.createModule(); +} + +fn sectionOnlyObj(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, name: []const u8, root: []const u8, com_asm: []const u8, com_inc: []const u8, files: []const []const u8) *std.Build.Step.Compile { + const obj = b.addObject(.{ + .name = name, + .root_module = b.createModule(.{ .target = target, .optimize = optimize }), + }); + obj.root_module.addIncludePath(.{ .cwd_relative = com_asm }); + obj.root_module.addIncludePath(.{ .cwd_relative = com_inc }); + obj.root_module.addCSourceFiles(.{ .root = .{ .cwd_relative = root }, .files = files }); + obj.lto = .none; + return obj; +} + +/// mega65/link.ld merged with commodore/commodore.ld, with the INPUT() +/// directives replaced by the objects linked above. +fn wrapperLd(b: *std.Build, sdk_root: []const u8) std.Build.LazyPath { + return b.addWriteFiles().add("mega65-wrapper.ld", b.fmt( + \\SEARCH_DIR("{0s}/mos-platform/mega65"); + \\SEARCH_DIR("{0s}/mos-platform/commodore"); + \\SEARCH_DIR("{0s}/mos-platform/common/ldscripts"); + \\__basic_zp_start = 0x0002; + \\__basic_zp_end = 0x0090; + \\MEMORY {{ ram (rw) : ORIGIN = 0x2001, LENGTH = 0xafff }} + \\__rc0 = __basic_zp_start; + \\INCLUDE "imag-regs.ld" + \\__basic_zp_size = __basic_zp_end - __basic_zp_start; + \\MEMORY {{ zp : ORIGIN = __rc31 + 1, LENGTH = __basic_zp_end - (__rc31 + 1) }} + \\REGION_ALIAS("c_readonly", ram) + \\REGION_ALIAS("c_writeable", ram) + \\SECTIONS {{ .basic_header : {{ *(.basic_header) }} INCLUDE "c.ld" }} + \\__stack = 0xd000; + \\OUTPUT_FORMAT {{ SHORT(0x2001) TRIM(ram) }} + , .{sdk_root})); +} diff --git a/zig/plasma/build.zig.zon b/zig/plasma/build.zig.zon new file mode 100644 index 0000000..f5ca27d --- /dev/null +++ b/zig/plasma/build.zig.zon @@ -0,0 +1,21 @@ +.{ + .name = .plasma, + .version = "0.0.1", + .minimum_zig_version = "0.17.0-mos", + .paths = .{""}, + .fingerprint = 0xd9e93465984bd5d0, + .dependencies = .{ + // Startup assembly, linker scripts and libc sources for the MEGA65 + // platform. Zig compiles all of these itself; no llvm-mos toolchain + // installation is required. + .@"llvm-mos-sdk" = .{ + .url = "git+https://github.com/llvm-mos/llvm-mos-sdk#3f6968bbc156ff9a63102a8e158db868819bd61c", + .hash = "N-V-__8AAKSeLAA0M14Y-qtR7Srg-0spUC9lk4H3234zgjc1", + }, + // C headers translated into Zig. + .@"mega65-libc" = .{ + .url = "git+https://github.com/mega65/mega65-libc#49f2f115d3d45da9475f00a34250587df472fe3a", + .hash = "N-V-__8AAEi4BQCsQLqubPd2GuG1KltNMZ_pi0pkpZiczpfs", + }, + }, +} diff --git a/zig/plasma/info.json b/zig/plasma/info.json new file mode 100644 index 0000000..40453f5 --- /dev/null +++ b/zig/plasma/info.json @@ -0,0 +1,21 @@ +{ + "title": "Plasma effect in Zig,", + "description": "A demo-inspired plasma effect ported to zig", + "author": "The MEGA65 Community", + "keywords": [ + "plasma", + "charset", + "zig" + ], + "languages": [ + "zig" + ], + "requirements": [ + "zig-mos" + ], + "rom": 920376, + "prgs": [ + "plasma.prg" + ], + "build": "zig build" +} diff --git a/zig/plasma/src/plasma.zig b/zig/plasma/src/plasma.zig new file mode 100644 index 0000000..eac84f0 --- /dev/null +++ b/zig/plasma/src/plasma.zig @@ -0,0 +1,99 @@ +//! MEGA65 plasma effect, a zig port of c/plasma/plasma.c. +//! +//! Sourced from cc65/samples/cbm: 2001 by groepaz, cleaned up and ported by +//! Ullrich von Bassewitz, MEGA65 adaptation by wombat (2022), which also drops +//! the clock to 3.5 MHz. +//! +//! The arithmetic wraps, as it does in the C original. +const mega65 = @import("mega65"); + +const COLS = 80; +const ROWS = 25; +const SINESIZE = 256; +const SCREEN_ADDRESS = 0x0800; +const CHARSET_ADDRESS = 0x3000; +const VICIV_CONTROLB = 0xd031; +const VICIV_CONTROLC = 0xd054; + +const screen: [*]volatile u8 = @ptrFromInt(SCREEN_ADDRESS); +const charset: [*]volatile u8 = @ptrFromInt(CHARSET_ADDRESS); + +fn mmio(comptime addr: usize) *volatile u8 { + return @ptrFromInt(addr); +} + +const sinustable = [SINESIZE]u8{ + 0x80, 0x7d, 0x7a, 0x77, 0x74, 0x70, 0x6d, 0x6a, 0x67, 0x64, 0x61, 0x5e, 0x5b, 0x58, 0x55, 0x52, 0x4f, 0x4d, 0x4a, 0x47, + 0x44, 0x41, 0x3f, 0x3c, 0x39, 0x37, 0x34, 0x32, 0x2f, 0x2d, 0x2b, 0x28, 0x26, 0x24, 0x22, 0x20, 0x1e, 0x1c, 0x1a, 0x18, + 0x16, 0x15, 0x13, 0x11, 0x10, 0x0f, 0x0d, 0x0c, 0x0b, 0x0a, 0x08, 0x07, 0x06, 0x06, 0x05, 0x04, 0x03, 0x03, 0x02, 0x02, + 0x02, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x03, 0x03, 0x04, 0x05, 0x06, 0x06, 0x07, 0x08, 0x0a, + 0x0b, 0x0c, 0x0d, 0x0f, 0x10, 0x11, 0x13, 0x15, 0x16, 0x18, 0x1a, 0x1c, 0x1e, 0x20, 0x22, 0x24, 0x26, 0x28, 0x2b, 0x2d, + 0x2f, 0x32, 0x34, 0x37, 0x39, 0x3c, 0x3f, 0x41, 0x44, 0x47, 0x4a, 0x4d, 0x4f, 0x52, 0x55, 0x58, 0x5b, 0x5e, 0x61, 0x64, + 0x67, 0x6a, 0x6d, 0x70, 0x74, 0x77, 0x7a, 0x7d, 0x80, 0x83, 0x86, 0x89, 0x8c, 0x90, 0x93, 0x96, 0x99, 0x9c, 0x9f, 0xa2, + 0xa5, 0xa8, 0xab, 0xae, 0xb1, 0xb3, 0xb6, 0xb9, 0xbc, 0xbf, 0xc1, 0xc4, 0xc7, 0xc9, 0xcc, 0xce, 0xd1, 0xd3, 0xd5, 0xd8, + 0xda, 0xdc, 0xde, 0xe0, 0xe2, 0xe4, 0xe6, 0xe8, 0xea, 0xeb, 0xed, 0xef, 0xf0, 0xf1, 0xf3, 0xf4, 0xf5, 0xf6, 0xf8, 0xf9, + 0xfa, 0xfa, 0xfb, 0xfc, 0xfd, 0xfd, 0xfe, 0xfe, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xfe, 0xfe, 0xfd, + 0xfd, 0xfc, 0xfb, 0xfa, 0xfa, 0xf9, 0xf8, 0xf6, 0xf5, 0xf4, 0xf3, 0xf1, 0xf0, 0xef, 0xed, 0xeb, 0xea, 0xe8, 0xe6, 0xe4, + 0xe2, 0xe0, 0xde, 0xdc, 0xda, 0xd8, 0xd5, 0xd3, 0xd1, 0xce, 0xcc, 0xc9, 0xc7, 0xc4, 0xc1, 0xbf, 0xbc, 0xb9, 0xb6, 0xb3, + 0xb1, 0xae, 0xab, 0xa8, 0xa5, 0xa2, 0x9f, 0x9c, 0x99, 0x96, 0x93, 0x90, 0x8c, 0x89, 0x86, 0x83, +}; + +var xbuffer: [COLS]u8 = undefined; +var ybuffer: [ROWS]u8 = undefined; +var c1A: u8 = 0; +var c1B: u8 = 0; +var c2A: u8 = 0; +var c2B: u8 = 0; + +fn draw() void { + var c1a = c1A; + var c1b = c1B; + for (&ybuffer) |*yb| { + yb.* = sinustable[c1a] +% sinustable[c1b]; + c1a = c1a +% 4; + c1b = c1b +% 9; + } + c1A = c1A +% 3; + c1B = c1B -% 5; + + var c2a = c2A; + var c2b = c2B; + for (&xbuffer) |*xb| { + xb.* = sinustable[c2a] +% sinustable[c2b]; + c2a = c2a +% 3; + c2b = c2b +% 7; + } + c2A = c2A +% 2; + c2B = c2B -% 3; + + // Bounded row/col: no overflow check, so Debug links without an `abort`. + for (ybuffer, 0..) |yb, row| { + for (xbuffer, 0..) |xb, col| screen[row * COLS + col] = xb +% yb; + } +} + +fn generate_charset() void { + const bits = [_]u8{ 1, 2, 4, 8, 16, 32, 64, 128 }; + for (sinustable, 0..) |sine, cnt| { + for (0..8) |i| { + var char_pattern: u8 = 0; + for (0..8) |j| { + if (mega65.rand8(0xff) > sine) char_pattern |= bits[j]; + } + charset[cnt * 8 + i] = char_pattern; + } + } +} + +/// Set MEGA65 speed to 3.5 Mhz. +fn speed_mode3() void { + mmio(VICIV_CONTROLB).* |= 0b01000000; // set FAST bit + mmio(VICIV_CONTROLC).* &= 0b10111111; // unset VFAST +} + +export fn main() void { + generate_charset(); + speed_mode3(); + mega65.setcharsetaddr(CHARSET_ADDRESS); + while (true) draw(); +} diff --git a/zig/plasma/zig-out/bin/plasma.prg b/zig/plasma/zig-out/bin/plasma.prg new file mode 100644 index 0000000..f703b43 Binary files /dev/null and b/zig/plasma/zig-out/bin/plasma.prg differ