Zig64

Check-in [b1f425d583]
Login

Check-in [b1f425d583]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Add some memory-related helpers and make `print()` follow the same API as other Zig `print()`s
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: b1f425d58333ee932c47790de2b3c2f277c815e96139191ece8820d4d3a73c01
User & Date: ryno 2025-02-04 01:05:40
About

Now that Debug.print() follows the same interface as std.Writer.print(), all the pieces are in place to do some basic print-debugging. This is also a critical step to getting stack traces working for the panic handler; that'll probably be the next major milestone.

I've also started shifting the memory-related functions to a dedicated memory namespace; the existing functions in PI will point there soon, and when I get around to implementing SI that'll be the same deal.

Testing has revealed that @memcpy is good enough for RDRAM and RCP writes, since they're synchronous and since @memcpy seems to be smart enough to write whole words at a time. @memcpy is still unsuitable for PI and SI writes, since it has no means of waiting for previous writes to finish before starting new ones. It's also still risky for RCP writes, since there's no enforcement of write targets starting and ending on word boundaries.

Context
2025-02-04
06:59
Finish memory operation refactor check-in: 8aaa187e2c user: ryno tags: trunk
01:05
Add some memory-related helpers and make `print()` follow the same API as other Zig `print()`s check-in: b1f425d583 user: ryno tags: trunk
2025-01-30
10:29
Put literally anything on the screen check-in: d5c698fc12 user: ryno tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to src/ISViewer.zig.


1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21



22
23
24

const PI = @import("./PI.zig");


/// Writing to this register will cause the (emulated) IS-Viewer to
/// read the specified number of bytes from `ISViewer.buffer` and
/// display them in the emulator's text output.
pub const write_len: *volatile u32 = @ptrFromInt(0xb3ff0014);
/// Buffer to store text to be sent via (emulated) IS-Viewer to the
/// emulator.
pub const buffer: *align(4) [0x200]u8 = @ptrFromInt(0xb3ff0020);

/// If true, an IS-Viewer (or an emulation thereof) is available.
/// Otherwise, false.
pub fn present() bool {
    PI.wait();
    buffer[0] = 0x12;
    PI.wait();
    return (buffer[0] == 0x12);
}

/// Sends text via the (emulated) IS-Viewer.
pub fn print(text: []const u8) void {



    PI.writeBytes(buffer, text);
    PI.writeWord(write_len, text.len);
}
>

>


















|
|
>
>
>
|
|

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const std = @import("std");
const PI = @import("./PI.zig");
const memory = @import("./memory.zig");

/// Writing to this register will cause the (emulated) IS-Viewer to
/// read the specified number of bytes from `ISViewer.buffer` and
/// display them in the emulator's text output.
pub const write_len: *volatile u32 = @ptrFromInt(0xb3ff0014);
/// Buffer to store text to be sent via (emulated) IS-Viewer to the
/// emulator.
pub const buffer: *align(4) [0x200]u8 = @ptrFromInt(0xb3ff0020);

/// If true, an IS-Viewer (or an emulation thereof) is available.
/// Otherwise, false.
pub fn present() bool {
    PI.wait();
    buffer[0] = 0x12;
    PI.wait();
    return (buffer[0] == 0x12);
}

/// Prints via the (emulated) IS-Viewer.
pub fn print(comptime fmt: []const u8, args: anytype) void {
    var scratch: [4096]u8 = undefined;
    var wrapper = std.io.fixedBufferStream(scratch[0..]);
    wrapper.writer().print(fmt, args) catch {};
    PI.writeBytes(buffer, scratch[0..wrapper.pos]);
    PI.writeWord(write_len, wrapper.pos);
}

Changes to src/SC64.zig.


1
2
3
4
5
6
7

const PI = @import("./PI.zig");

/// SC64 status/command register fields.
const Status = packed struct(u32) {
    // I sure hope I got the bit ordering right...
    /// ID of the command to execute.
    command_id: u8, // TODO: enum of valid command IDs
>







1
2
3
4
5
6
7
8
const std = @import("std");
const PI = @import("./PI.zig");

/// SC64 status/command register fields.
const Status = packed struct(u32) {
    // I sure hope I got the bit ordering right...
    /// ID of the command to execute.
    command_id: u8, // TODO: enum of valid command IDs
131
132
133
134
135
136
137
138



139
140
141
142
143
144
145
146
147
148
    /// value is `1`.
    datatype: u8 = 1,
};

/// Sends text via the SC64's USB port.  If the SC64 is connected to a
/// PC and the PC is running `sc64deployer debug`, the text will
/// display in the debug output.
pub fn print(text: []const u8) void {



    PI.writeBytes(data_buffer, text);
    PI.writeWord(data_0, @intFromPtr(data_buffer));
    const params: USBWriteParams = .{.length = @truncate(text.len)};
    PI.writeWord(data_1, @bitCast(params));
    PI.wait();
    status.command_id = 'M';
    PI.wait();
    var timeout: u8 = 0;
    while (usbBusy()) {
        if (timeout == 255) return;







|
>
>
>
|

|







132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
    /// value is `1`.
    datatype: u8 = 1,
};

/// Sends text via the SC64's USB port.  If the SC64 is connected to a
/// PC and the PC is running `sc64deployer debug`, the text will
/// display in the debug output.
pub fn print(comptime fmt: []const u8, args: anytype) void {
    var scratch: [4096]u8 = undefined;
    var wrapper = std.io.fixedBufferStream(scratch[0..]);
    wrapper.writer().print(fmt, args) catch {};
    PI.writeBytes(data_buffer, scratch[0..wrapper.pos]);
    PI.writeWord(data_0, @intFromPtr(data_buffer));
    const params: USBWriteParams = .{.length = @truncate(wrapper.pos)};
    PI.writeWord(data_1, @bitCast(params));
    PI.wait();
    status.command_id = 'M';
    PI.wait();
    var timeout: u8 = 0;
    while (usbBusy()) {
        if (timeout == 255) return;

Changes to src/main.zig.

23
24
25
26
27
28
29







30
31
32
33
34
35
36
/// "IS-Viewer-compatible" debug messaging support; known to work with
/// Ares, and Cen64 and Simple64 should work with it in theory (but
/// this is untested).  The SummerCart 64 also apparently has
/// IS-Viewer compatibility, though this is disabled by default (and
/// redundant anyway, given that Zig64 already natively supports the
/// SC64 via the `SC64` namespace).
const ISViewer = @import("./ISViewer.zig");








// END MEMORY MAP STUFF

// BEGIN DEBUG STUFF

/// Debug helper functions.
const Debug = struct {







>
>
>
>
>
>
>







23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/// "IS-Viewer-compatible" debug messaging support; known to work with
/// Ares, and Cen64 and Simple64 should work with it in theory (but
/// this is untested).  The SummerCart 64 also apparently has
/// IS-Viewer compatibility, though this is disabled by default (and
/// redundant anyway, given that Zig64 already natively supports the
/// SC64 via the `SC64` namespace).
const ISViewer = @import("./ISViewer.zig");

const Bus = @import("./memory.zig").Bus;

comptime {
    std.debug.assert(Bus.identify(SC64.data_buffer) == .PI);
    std.debug.assert(Bus.identify(dmem) == .RCP);
}

// END MEMORY MAP STUFF

// BEGIN DEBUG STUFF

/// Debug helper functions.
const Debug = struct {
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103

104
105
106
107
108
109
110
111
112
113
114
115
116
117
118

    /// Detects and sets the available backend.
    fn init() void {
        backend = detectBackend();
    }

    /// Sends text via the detected debug logging backend.
    fn print(text: []const u8) void {
        switch (backend) {
            .Dummy => {},
            .ISViewer => ISViewer.print(text),
            .SC64 => SC64.print(text),
            .ED64 => {}, // FIXME: implement
            .@"64Drive" => {}, // FIXME: implement
            .IQue => {}, // FIXME: implement
        }
    }
};

// END DEBUG STUFF

// BEGIN EVERYTHING ELSE

/// N64 ROM entry point.  IPL3 calls this function after initializing
/// RDRAM and loading our code into it.
export fn __start() linksection(".boot") noreturn {
    Debug.init();
    Debug.print("All your Nintendo 64 are belong to us.\n");
    Debug.print("This is another message from Zig.\n");
    const dmem_test_pat: [16]u8 align(4) = .{
        0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
        0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe
    };
    PI.writeBytes(dmem, &dmem_test_pat);
    const pxl: VI.Pixel16 = .{ .r = 31, .g = 31, .b = 0, .a = 0 };
    const test_framebuffer: [320][240]VI.Pixel16 = .{.{pxl} ** 240} ** 320;
    VI.origin.* = @intFromPtr(&test_framebuffer);
    VI.setup(.{});

    while (true) {}
}

pub fn panic(
    msg: []const u8,
    _: ?*std.builtin.StackTrace,
    _: ?usize
) noreturn {
    Debug.print("PANIC: ");
    Debug.print(msg);
    Debug.print("\n");
    while (true) {}
}

// END EVERYTHING ELSE







|


|
|















|
|




|




>
|







|
<
<




73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120


121
122
123
124

    /// Detects and sets the available backend.
    fn init() void {
        backend = detectBackend();
    }

    /// Sends text via the detected debug logging backend.
    fn print(comptime fmt: []const u8, args: anytype) void {
        switch (backend) {
            .Dummy => {},
            .ISViewer => ISViewer.print(fmt, args),
            .SC64 => SC64.print(fmt, args),
            .ED64 => {}, // FIXME: implement
            .@"64Drive" => {}, // FIXME: implement
            .IQue => {}, // FIXME: implement
        }
    }
};

// END DEBUG STUFF

// BEGIN EVERYTHING ELSE

/// N64 ROM entry point.  IPL3 calls this function after initializing
/// RDRAM and loading our code into it.
export fn __start() linksection(".boot") noreturn {
    Debug.init();
    Debug.print("All your Nintendo 64 are belong to us.\n", .{});
    Debug.print("This is another message from Zig.\n", .{});
    const dmem_test_pat: [16]u8 align(4) = .{
        0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
        0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe
    };
    @memcpy(dmem[0..16], &dmem_test_pat);
    const pxl: VI.Pixel16 = .{ .r = 31, .g = 31, .b = 0, .a = 0 };
    const test_framebuffer: [320][240]VI.Pixel16 = .{.{pxl} ** 240} ** 320;
    VI.origin.* = @intFromPtr(&test_framebuffer);
    VI.setup(.{});
    @panic("the demo is over already :(");
    // while (true) {}
}

pub fn panic(
    msg: []const u8,
    _: ?*std.builtin.StackTrace,
    _: ?usize
) noreturn {
    Debug.print("PANIC: {s}\n", .{msg});


    while (true) {}
}

// END EVERYTHING ELSE

Added src/memory.zig.



































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
const std = @import("std");
const PI = @import("./PI.zig");

pub const Bus = enum {
    RDRAM,
    RCP,
    PI,
    SI,

    /// Identifies which bus/device (RDRAM, RCP, PI, SI) corresponds
    /// to the given address.  This matters because different memory
    /// regions have different behaviors:
    ///
    /// - RDRAM and RCP are synchronous; PI and SI are asynchronous
    ///
    /// - RDRAM allows non-32-bit writes; RCP, PI, and SI only allow
    ///   32-bit writes
    pub fn identify(ptr: *anyopaque) Bus {
        var addr = @intFromPtr(ptr);
        // First, convert virtual addresses to physical addresses.
        // FIXME: there's probably a better way to do this, but my
        // bitwise arithmetic is a bit rusty and this is easier to
        // reason about.
        if (addr >= 0xe0000000) addr -= 0xe0000000;
        if (addr >= 0xc0000000) addr -= 0xc0000000;
        if (addr >= 0xa0000000) addr -= 0xa0000000;
        if (addr >= 0x80000000) addr -= 0x80000000;
        // Now we have an actual physical address to map to a physical
        // bus.
        if (0x00000000 <= addr and addr <= 0x03ffffff) return .RDRAM;
        if (0x04000000 <= addr and addr <= 0x04ffffff) return .RCP;
        if (0x05000000 <= addr and addr <= 0x1fbfffff) return .PI;
        if (0x1fc00000 <= addr and addr <= 0x1fcfffff) return .SI;
        if (0x1fd00000 <= addr and addr <= 0x7fffffff) return .PI;
        // Physical address 0x80000000 and beyond is mapped to
        // nothing.  Writes are ignored, and reads will stall the RCP
        // and therefore the CPU.  We've already guaranteed above that
        // we'll never make it to this point (because `addr` should
        // always be less than 0x80000000 after the virtual→physical
        // conversion), hence the `unreachable`.
        unreachable;
    }
};

/// Based on which bus handles the given pointer's physical address,
/// dispatches to a bus-specific wait mechanism (if one is
/// applicable).
pub inline fn wait(bus: Bus) void {
    switch (bus) {
        .RDRAM => {},
        .RCP => {},
        .PI => PI.wait(),
        .SI => {}, // FIXME: implement
    }
}

/// Writes four bytes at a time from src to dest.  Mostly
/// "borrowed" from std.Progress.copyAtomicStore(), which just so
/// happens to do more or less the same thing.
pub fn writeBytes(bus: Bus, dest: []align(4) u8, src: []const u8) void {
    // "But why not just use the built-in Zig functions to copy
    // between byte arrays?" I can already hear you asking.  Well,
    // it's because the RCP's implementation of the SysAd bus is
    // "simplified" such that it has no concept of an "access size";
    // it can only read and write whole (32-bit) words.  Want to write
    // a single byte?  lol no, fuck you, you're writing 32 bits
    // whether you like it or not, and which byte-sized chunk of that
    // word actually gets set (and which bytes get entirely clobbered)
    // is apparently non-deterministic.
    //
    // Or at least that's my impression from reading through
    // https://n64brew.dev/wiki/Memory_map (see the section on RCP
    // registers), and that impression seems to be correct given that
    // this behaves approximately as expected (albeit very unsafely)
    // while byte-by-byte copies only manage to preserve one of every
    // four bytes (both on real hardware and on bug-for-bug LLEs like
    // Ares).
    std.debug.assert(dest.len >= src.len);
    const chunked_len = src.len / 4;
    std.debug.assert((dest.len / 4) + 1 >= chunked_len);
    const dest_chunked: []u32 = @as([*]u32, @ptrCast(dest))[0..chunked_len + 1];
    for (dest_chunked[0..chunked_len], 0..) |*d, i| {
        const s = bytesToWord(src[(i*4)..]);
        writeWord(bus, d, s);
    }
    const extra = bytesToWord(src[(chunked_len * 4)..]);
    writeWord(bus, &dest_chunked[chunked_len], extra);
}

/// Creates a u32 from the first four of the provided bytes, filling
/// in with zeroes if there are less than four bytes.
pub fn bytesToWord(bytes: []const u8) u32 {
    // FIXME: there's probably a much better way to do this.
    var buf: [4]u8 = .{0,0,0,0};
    if (bytes.len >= 1) buf[0] = bytes[0];
    if (bytes.len >= 2) buf[1] = bytes[1];
    if (bytes.len >= 3) buf[2] = bytes[2];
    if (bytes.len >= 4) buf[3] = bytes[3];
    return std.mem.readInt(u32, &buf, .big);
}

/// Waits for the PI to be ready, then writes a word to a pointer.
pub fn writeWord(bus: Bus, pointer: *volatile u32, data: u32) void {
    wait(bus);
    @atomicStore(u32, pointer, data, .monotonic);
}

/// Waits for the PI to be ready, then reads a word from a
/// pointer.
pub fn readWord(bus: Bus, pointer: *volatile u32) u32 {
    wait(bus);
    return @atomicLoad(u32, pointer, .monotonic);
}