Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
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: |
b1f425d58333ee932c47790de2b3c2f2 |
User & Date: | ryno 2025-02-04 01:05:40 |
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.
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 to src/ISViewer.zig.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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); } | > > | | > > > | | | 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 | /// 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. | | > > > | | | 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 | /// Detects and sets the available backend. fn init() void { backend = detectBackend(); } /// Sends text via the detected debug logging backend. | | | | | | | > | | < < | 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); } |