Skip to content
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
95 changes: 95 additions & 0 deletions .github/workflows/zig.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,6 @@ cpm-package-lock.cmake
# rust
target/
Cargo.lock

# zig
*zig-*/
45 changes: 45 additions & 0 deletions zig/hello/README.md
Original file line number Diff line number Diff line change
@@ -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`
100 changes: 100 additions & 0 deletions zig/hello/build.zig
Original file line number Diff line number Diff line change
@@ -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}));
}
14 changes: 14 additions & 0 deletions zig/hello/build.zig.zon
Original file line number Diff line number Diff line change
@@ -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",
},
},
}
21 changes: 21 additions & 0 deletions zig/hello/info.json
Original file line number Diff line number Diff line change
@@ -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"
}
18 changes: 18 additions & 0 deletions zig/hello/src/hello.zig
Original file line number Diff line number Diff line change
@@ -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
}
Binary file added zig/hello/zig-out/bin/hello.prg
Binary file not shown.
46 changes: 46 additions & 0 deletions zig/plasma/README.md
Original file line number Diff line number Diff line change
@@ -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`
Loading