From 6fba27b780a1f4f08748b35651cdb3add6d814d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=8B=E6=96=87=E6=AD=A6?= Date: Sun, 20 Jul 2025 21:52:12 +0800 Subject: [PATCH] programs: Add Uxn emulator. --- data/Tupfile.lua | 1 + data/common/File Managers/icons.ini | 1 + data/common/File Managers/kfar.ini | 1 + data/common/settings/assoc.ini | 1 + programs/emulator/uxn/.gitignore | 6 + programs/emulator/uxn/README | 17 + programs/emulator/uxn/build.zig | 36 ++ programs/emulator/uxn/build.zig.zon | 21 + programs/emulator/uxn/src/kolibri.zig | 604 ++++++++++++++++++++++++++ programs/emulator/uxn/src/linker.ld | 42 ++ programs/emulator/uxn/src/main.zig | 418 ++++++++++++++++++ programs/emulator/uxn/uxn | Bin 0 -> 59569 bytes 12 files changed, 1148 insertions(+) create mode 100644 programs/emulator/uxn/.gitignore create mode 100644 programs/emulator/uxn/README create mode 100644 programs/emulator/uxn/build.zig create mode 100644 programs/emulator/uxn/build.zig.zon create mode 100644 programs/emulator/uxn/src/kolibri.zig create mode 100644 programs/emulator/uxn/src/linker.ld create mode 100644 programs/emulator/uxn/src/main.zig create mode 100755 programs/emulator/uxn/uxn diff --git a/data/Tupfile.lua b/data/Tupfile.lua index efb5c30ad..bd538ab7a 100644 --- a/data/Tupfile.lua +++ b/data/Tupfile.lua @@ -205,6 +205,7 @@ extra_files = { {"kolibrios/emul/chip8/roms/", SRC_PROGS .. "/emulator/chip8/roms/*"}, {"kolibrios/emul/kwine/kwine", SRC_PROGS .. "/emulator/kwine/bin/kwine"}, {"kolibrios/emul/kwine/lib/", SRC_PROGS .. "/emulator/kwine/bin/lib/*"}, + {"kolibrios/emul/uxn", SRC_PROGS .. "/emulator/uxn/uxn"}, {"kolibrios/emul/uarm/", "common/emul/uarm/*"}, {"kolibrios/emul/zsnes/", "common/emul/zsnes/*"}, {"kolibrios/games/BabyPainter", "common/games/BabyPainter"}, diff --git a/data/common/File Managers/icons.ini b/data/common/File Managers/icons.ini index e211975f0..f1d8c08d0 100644 --- a/data/common/File Managers/icons.ini +++ b/data/common/File Managers/icons.ini @@ -162,6 +162,7 @@ min=23 nes=23 sna=23 snes=23 +rom=23 bat=24 sh=24 sys=25 diff --git a/data/common/File Managers/kfar.ini b/data/common/File Managers/kfar.ini index f9a7e8e0d..a523ad4bf 100644 --- a/data/common/File Managers/kfar.ini +++ b/data/common/File Managers/kfar.ini @@ -68,6 +68,7 @@ sna=/kolibrios/emul/e80/e80 gb=/kolibrios/emul/gameboy gbc=/kolibrios/emul/gameboy min=/kolibrios/emul/pokemini +rom=/kolibrios/emul/uxn nc=/kolibrios/utils/cnc_editor/cnc_editor kf=/sys/KF_VIEW csv=/sys/table diff --git a/data/common/settings/assoc.ini b/data/common/settings/assoc.ini index ba2bab92f..f8bf1f9bf 100644 --- a/data/common/settings/assoc.ini +++ b/data/common/settings/assoc.ini @@ -192,6 +192,7 @@ nc=/kolibrios/utils/cnc_editor/cnc_editor ch8=/kolibrios/emul/chip8/chip8 md=/kolibrios/emul/dgen/dgen gen=/kolibrios/emul/dgen/dgen +rom=/kolibrios/emul/uxn zip=$Unz 7z=$Unz diff --git a/programs/emulator/uxn/.gitignore b/programs/emulator/uxn/.gitignore new file mode 100644 index 000000000..bebe790e0 --- /dev/null +++ b/programs/emulator/uxn/.gitignore @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: 2025 iyzsong@envs.net +# +# SPDX-License-Identifier: MPL-2.0 + +zig-out +.zig-cache diff --git a/programs/emulator/uxn/README b/programs/emulator/uxn/README new file mode 100644 index 000000000..4b95d1408 --- /dev/null +++ b/programs/emulator/uxn/README @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2025 iyzsong@envs.net +// +// SPDX-License-Identifier: MPL-2.0 + +Uxn/Varvara emulator for Kolibri OS + +Based on https://github.com/chmod222/zuxn + +compile: zig build --release=fast +result: zig-out/bin/uxn +run: uxn SOME.rom +control: + Up/Down/Left/Right + Ctrl/Shift/Alt/Home + F1: change scale (1x, 2x, 3x) + +TODO: file/directory stat, audio latency, open dialog? diff --git a/programs/emulator/uxn/build.zig b/programs/emulator/uxn/build.zig new file mode 100644 index 000000000..e55e79d70 --- /dev/null +++ b/programs/emulator/uxn/build.zig @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2025 iyzsong@envs.net +// +// SPDX-License-Identifier: MPL-2.0 + +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target_query = std.Target.Query{ + .cpu_arch = std.Target.Cpu.Arch.x86, + .os_tag = std.Target.Os.Tag.freestanding, + .abi = std.Target.Abi.none, + .cpu_model = std.Target.Query.CpuModel{ .explicit = &std.Target.x86.cpu.i586 }, + }; + const target = b.resolveTargetQuery(target_query); + const optimize = b.standardOptimizeOption(.{}); + const zuxn = b.dependency("zuxn", .{ + .target = target, + .optimize = optimize, + }); + const elf = b.addExecutable(.{ + .name = "uxn.elf", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .unwind_tables = .none, + }); + elf.root_module.addImport("uxn-core", zuxn.module("uxn-core")); + elf.root_module.addImport("uxn-varvara", zuxn.module("uxn-varvara")); + elf.setLinkerScript(b.path("src/linker.ld")); + const bin = elf.addObjCopy(.{ + .format = .bin, + }); + const install_bin = b.addInstallBinFile(bin.getOutput(), "uxn"); + b.getInstallStep().dependOn(&install_bin.step); + b.installArtifact(elf); +} diff --git a/programs/emulator/uxn/build.zig.zon b/programs/emulator/uxn/build.zig.zon new file mode 100644 index 000000000..6b7d2427f --- /dev/null +++ b/programs/emulator/uxn/build.zig.zon @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2025 iyzsong@envs.net +// +// SPDX-License-Identifier: MPL-2.0 + +.{ + .name = .uxn_kolibrios, + .version = "0.0.0", + .fingerprint = 0x3aef20f25c0a0218, + .minimum_zig_version = "0.14.0", + .dependencies = .{ + .zuxn = .{ + .url = "git+https://github.com/chmod222/zuxn.git#fc3a76724fa87dd08039438b56fc326ac3d51e4d", + .hash = "zuxn-0.0.1-XnoOpbqsAgD-fU6rv_AoLffA1utIzXuae2cmnHj6SzE6", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/programs/emulator/uxn/src/kolibri.zig b/programs/emulator/uxn/src/kolibri.zig new file mode 100644 index 000000000..da91bbcc6 --- /dev/null +++ b/programs/emulator/uxn/src/kolibri.zig @@ -0,0 +1,604 @@ +// SPDX-FileCopyrightText: 2025 iyzsong@envs.net +// +// SPDX-License-Identifier: MPL-2.0 + +const std = @import("std"); + +pub const SYS = enum(i32) { + terminate_process = -1, + create_window = 0, + put_pixel = 1, + get_key = 2, + get_sys_time = 3, + draw_text = 4, + sleep = 5, + put_image = 7, + define_button = 8, + thread_info = 9, + wait_event = 10, + check_event = 11, + redraw = 12, + draw_rect = 13, + get_screen_size = 14, + get_button = 17, + system = 18, + screen_put_image = 25, + system_get = 26, + get_sys_date = 29, + mouse_get = 37, + set_events_mask = 40, + style_settings = 48, + create_thread = 51, + board = 63, + keyboard = 66, + change_window = 67, + sys_misc = 68, + file = 70, + blitter = 73, +}; + +pub const Event = enum(u32) { + none = 0, + redraw = 1, + key = 2, + button = 3, + background = 5, + mouse = 6, + ipc = 7, +}; + +pub inline fn syscall0(number: SYS) u32 { + return asm volatile ("int $0x40" + : [ret] "={eax}" (-> u32), + : [number] "{eax}" (@intFromEnum(number)), + ); +} + +pub inline fn syscall1(number: SYS, arg1: u32) u32 { + return asm volatile ("int $0x40" + : [ret] "={eax}" (-> u32), + : [number] "{eax}" (@intFromEnum(number)), + [arg1] "{ebx}" (arg1), + ); +} + +pub inline fn syscall2(number: SYS, arg1: u32, arg2: u32) u32 { + return asm volatile ("int $0x40" + : [ret] "={eax}" (-> u32), + : [number] "{eax}" (@intFromEnum(number)), + [arg1] "{ebx}" (arg1), + [arg2] "{ecx}" (arg2), + ); +} + +pub inline fn syscall3(number: SYS, arg1: u32, arg2: u32, arg3: u32) u32 { + return asm volatile ("int $0x40" + : [ret] "={eax}" (-> u32), + : [number] "{eax}" (@intFromEnum(number)), + [arg1] "{ebx}" (arg1), + [arg2] "{ecx}" (arg2), + [arg3] "{edx}" (arg3), + ); +} + +pub inline fn syscall4(number: SYS, arg1: u32, arg2: u32, arg3: u32, arg4: u32) u32 { + return asm volatile ("int $0x40" + : [ret] "={eax}" (-> u32), + : [number] "{eax}" (@intFromEnum(number)), + [arg1] "{ebx}" (arg1), + [arg2] "{ecx}" (arg2), + [arg3] "{edx}" (arg3), + [arg4] "{esi}" (arg4), + ); +} + +pub inline fn syscall5(number: SYS, arg1: u32, arg2: u32, arg3: u32, arg4: u32, arg5: u32) u32 { + return asm volatile ("int $0x40" + : [ret] "={eax}" (-> u32), + : [number] "{eax}" (@intFromEnum(number)), + [arg1] "{ebx}" (arg1), + [arg2] "{ecx}" (arg2), + [arg3] "{edx}" (arg3), + [arg4] "{esi}" (arg4), + [arg5] "{edi}" (arg5), + ); +} + +pub fn terminateProcess() noreturn { + _ = syscall0(SYS.terminate_process); + unreachable; +} + +pub const WindowFlags = struct { + skinned: bool = true, + fixed: bool = true, + no_title: bool = false, + relative_coordinates: bool = false, + no_fill: bool = false, + unmovable: bool = false, +}; + +pub fn createWindow(x: u16, y: u16, width: u16, height: u16, bgcolor: u24, flags: WindowFlags, title: [*:0]const u8) void { + var f1: u32 = 0x00000000; + if (flags.no_fill) + f1 |= 0x40000000; + if (flags.relative_coordinates) + f1 |= 0x20000000; + if (!flags.no_title) + f1 |= 0x10000000; + if (flags.skinned) { + if (flags.fixed) { + f1 |= 0x04000000; + } else { + f1 |= 0x03000000; + } + } else { + f1 |= 0x01000000; + } + var f2: u32 = 0x00000000; + if (flags.unmovable) + f2 = 0x01000000; + _ = syscall5(SYS.create_window, @as(u32, x) * 0x10000 + width, @as(u32, y) * 0x10000 + height, f1 | @as(u32, bgcolor), f2 | @as(u32, bgcolor), @intFromPtr(title)); +} + +pub fn putPixel(x: u16, y: u16, color: u24) void { + _ = syscall3(SYS.put_pixel, x, y, color); +} + +pub fn invertPixel(x: u16, y: u16) void { + _ = syscall3(SYS.put_pixel, x, y, 0x01000000); +} + +pub const Key = packed struct(u32) { + _unused: u8 = 0, + key: u8 = 0, + scancode: u8 = 0, + empty: u8 = 1, + + pub fn pressed(self: *const Key) bool { + return self.key & 0x80 > 0; + } +}; + +pub fn getKey() Key { + return @bitCast(syscall0(SYS.get_key)); +} + +pub fn getSysTime() u24 { + return @intCast(syscall0(SYS.get_sys_time)); +} + +pub fn getButton() u32 { + return syscall0(SYS.get_button); +} + +pub fn terminateThreadId(id: u32) void { + _ = syscall2(SYS.system, 18, id); +} + +pub fn drawText(x: u16, y: u16, color: u24, text: [*:0]const u8) void { + _ = syscall5(SYS.draw_text, @as(u32, x) * 0x10000 + y, 0x80000000 | @as(u32, color), @intFromPtr(text), 0, 0); +} + +pub fn sleep(centisecond: u32) void { + _ = syscall1(SYS.sleep, centisecond); +} + +pub fn beginDraw() void { + _ = syscall1(SYS.redraw, 1); +} + +pub fn endDraw() void { + _ = syscall1(SYS.redraw, 2); +} + +pub fn putImage(image: [*]const u8, width: u16, height: u16, x: u16, y: u16) void { + _ = syscall3(SYS.put_image, @intFromPtr(image), @as(u32, width) * 0x10000 + height, @as(u32, x) * 0x10000 + y); +} + +pub fn drawRect(x: u16, y: u16, width: u16, height: u16, color: u24) void { + _ = syscall3(SYS.draw_rect, @as(u32, x) * 0x10000 + width, @as(u32, y) * 0x10000 + height, color); +} + +pub fn getScreenSize() packed struct(u32) { height: u16, width: u16 } { + return @bitCast(syscall0(SYS.get_screen_size)); +} + +pub fn waitEvent() Event { + return @enumFromInt(syscall0(SYS.wait_event)); +} + +pub fn checkEvent() Event { + return @enumFromInt(syscall0(SYS.check_event)); +} + +pub fn createThread(entry: *const fn () void, stack: []u8) u32 { + return syscall3(SYS.create_thread, 1, @intFromPtr(entry), @intFromPtr(stack.ptr) + stack.len); +} + +pub fn debugWrite(byte: u8) void { + _ = syscall2(SYS.board, 1, byte); +} + +pub fn debugWriteText(bytes: []const u8) void { + for (bytes) |byte| { + debugWrite(byte); + } +} + +pub const EventsMask = packed struct(u32) { + redraw: bool = true, // 0 + key: bool = true, + button: bool = true, + _reserved: bool = false, + background: bool = false, + mouse: bool = false, + ipc: bool = false, + network: bool = false, + debug: bool = false, + _unused: u23 = 0, +}; + +pub fn setEventsMask(mask: EventsMask) EventsMask { + return @bitCast(syscall1(SYS.set_events_mask, @bitCast(mask))); +} + +pub fn getSkinHeight() u16 { + return @intCast(syscall1(SYS.style_settings, 4)); +} + +pub fn screenPutImage(image: [*]const u32, width: u16, height: u16, x: u16, y: u16) void { + _ = syscall3(SYS.screen_put_image, @intFromPtr(image), @as(u32, width) * 0x10000 + height, @as(u32, x) * 0x10000 + y); +} + +pub fn systemGetTimeCount() u32 { + return syscall1(SYS.system_get, 9); +} + +pub fn getSysDate() u24 { + return @intCast(syscall0(SYS.get_sys_date)); +} + +pub fn mouseGetScreenPosition() packed struct(u32) { y: u16, x: u16 } { + return @bitCast(syscall1(SYS.mouse_get, 0)); +} + +pub fn mouseGetWindowPosition() packed struct(u32) { y: u16, x: u16 } { + return @bitCast(syscall1(SYS.mouse_get, 1)); +} + +pub fn loadCursorIndirect(image: *const [32 * 32]u32, spotx: u5, spoty: u5) u32 { + return syscall3(SYS.mouse_get, 4, @intFromPtr(image), 0x0002 | (@as(u32, spotx) << 24) | (@as(u32, spoty) << 16)); +} + +pub fn setCursor(cursor: u32) u32 { + return syscall2(SYS.mouse_get, 5, cursor); +} + +pub const MouseEvents = packed struct(u32) { + left_hold: bool = false, + right_hold: bool = false, + middle_hold: bool = false, + button4_hold: bool = false, + button5_hold: bool = false, + _unused0: u3 = 0, + left_pressed: bool = false, + right_pressed: bool = false, + middle_pressed: bool = false, + _unused1: u4 = 0, + vertical_scroll: bool = false, + left_released: bool = false, + right_released: bool = false, + middle_released: bool = false, + _unused2: u4 = 0, + horizontal_scroll: bool = false, + left_double_clicked: bool = false, + _unused3: u7 = 0, +}; + +pub fn mouseGetEvents() MouseEvents { + return @bitCast(syscall1(SYS.mouse_get, 3)); +} + +pub fn heapInit() u32 { + return syscall1(SYS.sys_misc, 11); +} + +pub fn memAlloc(size: u32) *anyopaque { + return @ptrFromInt(syscall2(SYS.sys_misc, 12, size)); +} + +pub fn memFree(ptr: *anyopaque) void { + _ = syscall2(SYS.sys_misc, 13, @intFromPtr(ptr)); +} + +pub fn memRealloc(size: u32, ptr: *anyopaque) *anyopaque { + return @ptrFromInt(syscall3(SYS.sys_misc, 20, size, @intFromPtr(ptr))); +} + +fn alloc(ctx: *anyopaque, len: usize, alignment: std.mem.Alignment, ret_addr: usize) ?[*]u8 { + _ = ctx; + _ = alignment; + _ = ret_addr; + return @ptrCast(memAlloc(len)); +} + +fn free(ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, ret_addr: usize) void { + _ = ctx; + _ = alignment; + _ = ret_addr; + memFree(@ptrCast(memory.ptr)); +} + +fn resize(ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, ret_addr: usize) bool { + _ = ctx; + _ = alignment; + _ = ret_addr; + _ = memRealloc(new_len, @ptrCast(memory.ptr)); + return true; +} + +fn remap(ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, ret_addr: usize) ?[*]u8 { + _ = ctx; + _ = memory; + _ = alignment; + _ = new_len; + _ = ret_addr; + return null; +} + +pub const allocator: std.mem.Allocator = .{ + .ptr = undefined, + .vtable = &.{ + .alloc = alloc, + .free = free, + .resize = resize, + .remap = remap, + }, +}; + +pub fn loadDriver(name: [*:0]const u8) u32 { + return syscall2(SYS.sys_misc, 16, @intFromPtr(name)); +} + +pub fn controlDriver(drv: u32, func: u32, in: ?[]const u32, out: ?[]const *anyopaque) u32 { + const ioctl: packed struct(u192) { + drv: u32, + func: u32, + inptr: u32, + insize: u32, + outptr: u32, + outsize: u32, + } = .{ + .drv = drv, + .func = func, + .inptr = if (in) |v| @intFromPtr(v.ptr) else 0, + .insize = if (in) |v| v.len * 4 else 0, + .outptr = if (out) |v| @intFromPtr(v.ptr) else 0, + .outsize = if (out) |v| v.len * 4 else 0, + }; + return syscall2(SYS.sys_misc, 17, @intFromPtr(&ioctl)); +} + +pub const Signal = packed struct(u192) { + kind: u32, + data0: u32, + data1: u32, + data2: u32, + data3: u32, + data4: u32, +}; + +pub fn waitSignal(sig: *Signal) void { + _ = syscall2(SYS.sys_misc, 14, @intFromPtr(sig)); +} + +pub const Sound = struct { + drv: u32, + + pub const Buffer = struct { + drv: u32, + handle: u32, + + pub fn play(self: *const Buffer, flags: u32) void { + _ = controlDriver(self.drv, 10, &.{ self.handle, flags }, null); + } + + pub fn set(self: *const Buffer, src: []u8, offset: u32) void { + _ = controlDriver(self.drv, 8, &.{ self.handle, @intFromPtr(src.ptr), offset, src.len }, null); + } + }; + + pub fn init() Sound { + return .{ + .drv = loadDriver("INFINITY"), + }; + } + + pub fn createBuffer(self: *const Sound, format: u32, size: u32) Buffer { + var ret: u32 = 0; + _ = controlDriver(self.drv, 1, &.{ format, size }, &.{&ret}); + return .{ + .drv = self.drv, + .handle = ret, + }; + } +}; + +pub const InputMode = enum(u32) { + normal = 0, + scancodes = 1, +}; + +pub fn setInputMode(mode: InputMode) void { + _ = syscall2(SYS.keyboard, 1, @intFromEnum(mode)); +} + +pub fn changeWindow(x: u32, y: u32, width: u32, height: u32) void { + _ = syscall4(SYS.change_window, x, y, width, height); +} + +pub const ControlKeys = packed struct(u32) { + left_shift: bool, + right_shift: bool, + left_ctrl: bool, + right_ctrl: bool, + left_alt: bool, + right_alt: bool, + caps_lock: bool, + num_lock: bool, + scroll_lock: bool, + left_win: bool, + right_win: bool, + _unused: u21, +}; + +pub fn getControlKeys() ControlKeys { + return @bitCast(syscall1(SYS.keyboard, 3)); +} + +const FileInfo = packed struct(u200) { + subfn: u32, + offset: u64, + size: u32, + buffer: u32, + path0: u8 = 0, + pathptr: *const u8, +}; + +pub fn readFile(pathname: [*:0]const u8, offset: u64, buffer: []u8) !u32 { + const info: FileInfo = .{ + .subfn = 0, + .offset = offset, + .size = buffer.len, + .buffer = @intFromPtr(buffer.ptr), + .pathptr = @ptrCast(pathname), + }; + const err = asm volatile ("int $0x40" + : [ret] "={eax}" (-> u32), + : [number] "{eax}" (SYS.file), + [info] "{ebx}" (&info), + ); + const size = asm volatile ("" + : [ret] "={ebx}" (-> u32), + ); + return switch (err) { + 0 => size, + 10 => error.AccessDenied, + 6 => size, + else => error.Unexpected, + }; +} + +pub fn createFile(pathname: [*:0]const u8, buffer: []u8) !u32 { + const info: FileInfo = .{ + .subfn = 2, + .offset = 0, + .size = buffer.len, + .buffer = @intFromPtr(buffer.ptr), + .pathptr = @ptrCast(pathname), + }; + const err = asm volatile ("int $0x40" + : [ret] "={eax}" (-> u32), + : [number] "{eax}" (SYS.file), + [info] "{ebx}" (&info), + ); + const size = asm volatile ("" + : [ret] "={ebx}" (-> u32), + ); + return switch (err) { + 0 => size, + 10 => error.AccessDenied, + 8 => size, + else => error.Unexpected, + }; +} + +pub fn writeFile(pathname: [*:0]const u8, offset: u64, buffer: []u8) !u32 { + const info: FileInfo = .{ + .subfn = 3, + .offset = offset, + .size = buffer.len, + .buffer = @intFromPtr(buffer.ptr), + .pathptr = @ptrCast(pathname), + }; + const err = asm volatile ("int $0x40" + : [ret] "={eax}" (-> u32), + : [number] "{eax}" (SYS.file), + [info] "{ebx}" (&info), + ); + const size = asm volatile ("" + : [ret] "={ebx}" (-> u32), + ); + return switch (err) { + 0 => size, + 10 => error.AccessDenied, + 5 => error.FileNotFound, + else => error.Unexpected, + }; +} + +pub fn fileGetSize(pathname: [*:0]const u8) !u64 { + var ret: [10]u32 = undefined; + const info: FileInfo = .{ + .subfn = 5, + .offset = 0, + .size = 0, + .buffer = @intFromPtr(&ret), + .pathptr = @ptrCast(pathname), + }; + if (syscall1(SYS.file, @intFromPtr(&info)) != 0) + return error.Unexpected; + return ret[8] | (@as(u64, ret[9]) << 32); +} + +pub fn deleteFile(pathname: [*:0]const u8) !void { + const info: FileInfo = .{ + .subfn = 8, + .offset = 0, + .size = 0, + .buffer = 0, + .pathptr = @ptrCast(pathname), + }; + const err = syscall1(SYS.file, @intFromPtr(&info)); + if (err != 0) + return error.Unexpected; +} + +pub const File = struct { + pathname: [*:0]const u8, + offset: u64 = 0, + + pub fn reader(file: *File) std.io.GenericReader(*File, anyerror, struct { + fn read(context: *File, buffer: []u8) !usize { + const size = try readFile(context.pathname, context.offset, buffer); + context.offset += size; + return size; + } + }.read) { + return .{ .context = file }; + } +}; + +pub const BlitterFlags = packed struct(u32) { + rop: u4 = 0, + background: bool = false, + transparent: bool = false, + reserved1: u23 = 0, + client_relative: bool = true, + reserved2: u2 = 0, +}; + +pub fn blitter(dstx: u32, dsty: u32, dstw: u32, dsth: u32, srcx: u32, srcy: u32, srcw: u32, srch: u32, src: *const u8, pitch: u32, flags: BlitterFlags) void { + _ = syscall2(SYS.blitter, @bitCast(flags), @intFromPtr(&[_]u32{ dstx, dsty, dstw, dsth, srcx, srcy, srcw, srch, @intFromPtr(src), pitch })); +} + +pub const DebugWriter = std.io.GenericWriter(void, anyerror, struct { + fn write(context: void, bytes: []const u8) !usize { + _ = context; + debugWriteText(bytes); + return bytes.len; + } +}.write); + +pub const debug_writer: DebugWriter = .{ .context = {} }; diff --git a/programs/emulator/uxn/src/linker.ld b/programs/emulator/uxn/src/linker.ld new file mode 100644 index 000000000..a83f3bf4a --- /dev/null +++ b/programs/emulator/uxn/src/linker.ld @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2025 iyzsong@envs.net + * + * SPDX-License-Identifier: MPL-2.0 + */ + +SECTIONS +{ + .text 0x00000000 : + { + /* MENUET01 header */ + LONG(0x554e454d); + LONG(0x31305445); + LONG(1); + LONG(_start); + LONG(_image_end); + LONG(_memory_end); + LONG(_stack_top); + LONG(_cmdline); + LONG(0); + *(.text) + *(.text.*) + } + .rodata : ALIGN(8) + { + *(.rodata) + *(.rodata.*) + } + .data : ALIGN(8) + { + *(.data) + } + _image_end = .; + + .bss : ALIGN(8) + { + *(.bss) + . = . + 4K; + _stack_top = .; + } + _memory_end = .; +} diff --git a/programs/emulator/uxn/src/main.zig b/programs/emulator/uxn/src/main.zig new file mode 100644 index 000000000..3a833df53 --- /dev/null +++ b/programs/emulator/uxn/src/main.zig @@ -0,0 +1,418 @@ +// SPDX-FileCopyrightText: 2025 iyzsong@envs.net +// +// SPDX-License-Identifier: MPL-2.0 + +const std = @import("std"); +const kos = @import("kolibri.zig"); +const uxn = @import("uxn-core"); +const varvara = @import("uxn-varvara"); + +const allocator = kos.allocator; +export var _cmdline: [1024]u8 = undefined; + +pub const std_options: std.Options = .{ + .log_level = .info, + .logFn = struct { + fn log(comptime level: std.log.Level, comptime scope: @Type(.enum_literal), comptime format: []const u8, args: anytype) void { + _ = level; + _ = scope; + kos.debug_writer.print(format, args) catch return; + } + }.log, +}; + +const VarvaraDefault = varvara.VarvaraSystem(kos.DebugWriter, kos.DebugWriter); +const emu = struct { + var cpu: uxn.Cpu = undefined; + var sys: VarvaraDefault = undefined; + var rom: *[0x10000]u8 = undefined; + var pixels: []u8 = undefined; + var screen_width: u32 = undefined; + var screen_height: u32 = undefined; + var null_cursor: u32 = undefined; + var hide_cursor: bool = false; + var audio_thread: ?u32 = null; + var scale: u4 = 1; + + fn init(rompath: [*:0]const u8) !void { + const screen = &emu.sys.screen_device; + var rom_file = kos.File{ .pathname = rompath }; + emu.rom = try uxn.loadRom(allocator, rom_file.reader()); + emu.cpu = uxn.Cpu.init(emu.rom); + emu.sys = try VarvaraDefault.init(allocator, kos.debug_writer, kos.debug_writer); + emu.cpu.device_intercept = struct { + var file_offsets: [2]u64 = .{ 0, 0 }; + + fn bcd8(x: u8) u8 { + return (x & 0xf) + 10 * ((x & 0xf0) >> 4); + } + + fn getFilePortSlice(dev: *varvara.file.File, comptime port: comptime_int) []u8 { + const ports = varvara.file.ports; + const ptr: usize = dev.loadPort(u16, &cpu, port); + + return if (port == ports.name) + std.mem.sliceTo(cpu.mem[ptr..], 0x00) + else + return cpu.mem[ptr..ptr +| dev.loadPort(u16, &cpu, ports.length)]; + } + + pub fn intercept(self: *uxn.Cpu, addr: u8, kind: uxn.Cpu.InterceptKind, data: ?*anyopaque) !void { + _ = data; + const port: u4 = @truncate(addr & 0x0f); + if (audio_thread == null and addr >= 0x30 and addr < 0x70) { + audio_thread = kos.createThread(&audio, allocator.alloc(u8, 32 * 1024) catch unreachable); + } + switch (addr >> 4) { + 0xa, 0xb => { + if (kind != .output) + return; + + const idx = (addr >> 4) - 0xa; + const dev = &sys.file_devices[idx]; + const ports = varvara.file.ports; + switch (port) { + ports.stat + 1 => { + // TODO: file/directory stat + dev.storePort(u16, &cpu, ports.success, 0); + }, + ports.delete => { + const name_slice = getFilePortSlice(dev, ports.name); + _ = kos.deleteFile(@ptrCast(name_slice)) catch {}; + dev.storePort(u16, &cpu, ports.success, 0); + }, + ports.name + 1 => { + file_offsets[idx] = 0; + dev.storePort(u16, &cpu, ports.success, 1); + }, + ports.read + 1 => { + const name_slice = getFilePortSlice(dev, ports.name); + const data_slice = getFilePortSlice(dev, ports.read); + const ret: u32 = kos.readFile(@ptrCast(name_slice), file_offsets[idx], data_slice) catch 0; + file_offsets[idx] += ret; + dev.storePort(u16, &cpu, ports.success, @intCast(ret)); + }, + ports.write + 1 => { + const append = dev.loadPort(u8, &cpu, ports.append) == 0x01; + const pathname: [*:0]const u8 = @ptrCast(getFilePortSlice(dev, ports.name)); + const buffer = getFilePortSlice(dev, ports.write); + var ret: u32 = 0; + if (append) { + const offset: u32 = @intCast(kos.fileGetSize(pathname) catch 0); + ret = kos.writeFile(pathname, offset, buffer) catch |err| blk: { + if (err == error.FileNotFound) { + break :blk kos.createFile(pathname, buffer) catch 0; + } else { + break :blk 0; + } + }; + } else { + ret = kos.createFile(pathname, buffer) catch 0; + } + dev.storePort(u16, &cpu, ports.success, @intCast(ret)); + }, + else => {}, + } + }, + 0xc => { + if (kind != .input) + return; + + const dev = &sys.datetime_device; + const date = kos.getSysDate(); + const time = kos.getSysTime(); + switch (port) { + 0x0, 0x1 => { + const year: u8 = bcd8(@truncate(date & 0xff)); + dev.storePort(u16, &cpu, 0x0, @as(u16, 2000) + bcd8(year)); + }, + 0x02 => { + const month: u8 = bcd8(@truncate((date & 0xff00) >> 8)); + dev.storePort(u8, &cpu, port, month); + }, + 0x03 => { + const day: u8 = bcd8(@truncate((date & 0xff0000) >> 16)); + dev.storePort(u8, &cpu, port, day); + }, + 0x04 => { + const hour: u8 = bcd8(@truncate(time & 0xff)); + dev.storePort(u8, &cpu, port, hour); + }, + 0x05 => { + const minute: u8 = bcd8(@truncate((time & 0xff00) >> 8)); + dev.storePort(u8, &cpu, port, minute); + }, + 0x06 => { + const second: u8 = bcd8(@truncate((time & 0xff0000) >> 16)); + dev.storePort(u8, &cpu, port, second); + }, + else => {}, + } + }, + else => try emu.sys.intercept(self, addr, kind), + } + } + }.intercept; + emu.cpu.output_intercepts = varvara.full_intercepts.output; + emu.cpu.input_intercepts = varvara.full_intercepts.input; + + try emu.cpu.evaluateVector(0x0100); + screen_width = screen.width * emu.scale; + screen_height = screen.height * emu.scale; + emu.pixels = try allocator.alloc(u8, @as(usize, 4) * screen_width * screen_height); + } + + fn exit() void { + if (audio_thread) |tid| { + kos.terminateThreadId(tid); + } + kos.terminateProcess(); + } + + fn audio() void { + var samples: [8192]i16 = undefined; + var sig: kos.Signal = undefined; + const buf = kos.Sound.init().createBuffer(3 | 0x10000000, 0); + buf.play(0); + while (true) { + kos.waitSignal(&sig); + if (sig.kind != 0xFF000001) continue; + @memset(&samples, 0); + for (0..samples.len / 512) |i| { + const w = samples[i * 512 .. (i + 1) * 512]; + for (&sys.audio_devices) |*poly| { + if (poly.duration <= 0) { + poly.evaluateFinishVector(&cpu) catch unreachable; + } + poly.updateDuration(); + poly.renderAudio(w); + } + } + for (0..samples.len) |i| { + samples[i] <<= 6; + } + buf.set(@ptrCast(&samples), sig.data2); + } + } + + fn update() !void { + const screen = &sys.screen_device; + const colors = &sys.system_device.colors; + if (sys.system_device.exit_code) |_| { + exit(); + } + if (screen_width != screen.width * scale or screen_height != screen.height * scale) { + const skin_height = kos.getSkinHeight(); + allocator.free(emu.pixels); + screen_width = screen.width * scale; + screen_height = screen.height * scale; + emu.pixels = try allocator.alloc(u8, @as(usize, 4) * screen_width * screen_height); + kos.changeWindow(100, 100, screen_width + 9, screen_height + skin_height + 4); + } + try screen.evaluateFrame(&cpu); + if (screen.dirty_region) |region| { + for (region.y0..region.y1) |y| { + for (region.x0..region.x1) |x| { + const idx = y * screen.width + x; + const pal = (@as(u4, screen.foreground[idx]) << 2) | screen.background[idx]; + const color = colors[if ((pal >> 2) > 0) (pal >> 2) else (pal & 0x3)]; + for (0..scale) |sy| { + for (0..scale) |sx| { + pixels[4 * ((y * scale + sy) * screen.width * scale + (x * scale + sx)) + 2] = color.r; + pixels[4 * ((y * scale + sy) * screen.width * scale + (x * scale + sx)) + 1] = color.g; + pixels[4 * ((y * scale + sy) * screen.width * scale + (x * scale + sx)) + 0] = color.b; + } + } + } + } + const ix = region.x0 * scale; + const iy = region.y0 * scale; + const iw = (region.x1 - region.x0) * scale; + const ih = (region.y1 - region.y0) * scale; + kos.blitter(ix, iy, iw, ih, ix, iy, iw, ih, @ptrCast(emu.pixels.ptr), screen.width * scale * 4, .{}); + screen.dirty_region = null; + } + } + + fn changeScale() void { + const screen = &sys.screen_device; + scale = switch (scale) { + 1 => 2, + 2 => 3, + 3 => 1, + else => 1, + }; + screen.forceRedraw(); + } +}; + +export fn _start() noreturn { + const cursor: [32 * 32]u32 = .{0} ** (32 * 32); + var counter: u32 = 0; + var last_tick = kos.systemGetTimeCount(); + + _ = kos.heapInit(); + _ = kos.setEventsMask(.{ .mouse = true }); + kos.setInputMode(.scancodes); + emu.null_cursor = kos.loadCursorIndirect(&cursor, 0, 0); + emu.init(@ptrCast(&_cmdline)) catch unreachable; + + const screen = &emu.sys.screen_device; + const controller = &emu.sys.controller_device; + + const callbacks = struct { + fn redraw() void { + const skin_height = kos.getSkinHeight(); + kos.beginDraw(); + kos.createWindow(300, 300, screen.width * emu.scale + 9, screen.height * emu.scale + skin_height + 4, 0x000000, .{ .skinned = true, .no_fill = true, .relative_coordinates = true }, "UXN"); + kos.endDraw(); + } + + fn key() void { + const symtab: [0x80]u8 = .{ + // 0x0* + 0, 27, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', 8, '\t', + // 0x1* + 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\r', 0, 'a', 's', + // 0x2* + 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', '`', 0, '\\', 'z', 'x', 'c', 'v', + // 0x3* + 'b', 'n', 'm', ',', '.', '/', 0, 0, 0, ' ', 0, 0, 0, 0, 0, 0, + // 0x00* + SHIFT + 0, 27, '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', 8, '\t', + // 0x10* + SHIFT + 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '\r', 0, 'A', 'S', + // 0x20* + SHIFT + 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '"', '~', 0, '|', 'Z', 'X', 'C', 'V', + // 0x30* + SHIFT, + 'B', 'N', 'M', '<', '>', '?', 0, 0, 0, ' ', 0, 0, 0, 0, 0, 0, + }; + const scancode1 = kos.getKey().key; + const input: ?union(enum) { + button: struct { + flags: varvara.controller.ButtonFlags, + pressed: bool, + }, + key: u8, + } = switch (scancode1) { + 0xe0 => blk: { + const scancode2 = kos.getKey().key; + break :blk switch (scancode2) { + 0x1d => .{ .button = .{ .flags = .{ .ctrl = true }, .pressed = true } }, + 0x9d => .{ .button = .{ .flags = .{ .ctrl = true }, .pressed = false } }, + 0x38 => .{ .button = .{ .flags = .{ .alt = true }, .pressed = true } }, + 0xb8 => .{ .button = .{ .flags = .{ .alt = true }, .pressed = false } }, + 0x47 => .{ .button = .{ .flags = .{ .start = true }, .pressed = true } }, + 0xc7 => .{ .button = .{ .flags = .{ .start = true }, .pressed = false } }, + 0x48 => .{ .button = .{ .flags = .{ .up = true }, .pressed = true } }, + 0xc8 => .{ .button = .{ .flags = .{ .up = true }, .pressed = false } }, + 0x50 => .{ .button = .{ .flags = .{ .down = true }, .pressed = true } }, + 0xd0 => .{ .button = .{ .flags = .{ .down = true }, .pressed = false } }, + 0x4b => .{ .button = .{ .flags = .{ .left = true }, .pressed = true } }, + 0xcb => .{ .button = .{ .flags = .{ .left = true }, .pressed = false } }, + 0x4d => .{ .button = .{ .flags = .{ .right = true }, .pressed = true } }, + 0xcd => .{ .button = .{ .flags = .{ .right = true }, .pressed = false } }, + else => null, + }; + }, + 0x3b => blk: { + emu.changeScale(); + break :blk null; + }, + 0x1d => .{ .button = .{ .flags = .{ .ctrl = true }, .pressed = true } }, + 0x9d => .{ .button = .{ .flags = .{ .ctrl = true }, .pressed = false } }, + 0x38 => .{ .button = .{ .flags = .{ .alt = true }, .pressed = true } }, + 0xb8 => .{ .button = .{ .flags = .{ .alt = true }, .pressed = false } }, + 0x2a, 0x36 => .{ .button = .{ .flags = .{ .shift = true }, .pressed = true } }, + 0xaa, 0xb6 => .{ .button = .{ .flags = .{ .shift = true }, .pressed = false } }, + else => blk: { + if (scancode1 > 0x40) { + break :blk null; + } + const ctrls = kos.getControlKeys(); + const k = if (ctrls.left_shift or ctrls.right_shift) symtab[scancode1 + 0x40] else symtab[scancode1]; + break :blk if (k > 0) .{ .key = k } else null; + }, + }; + + if (input) |v| { + switch (v) { + .button => { + if (v.button.pressed) { + controller.pressButtons(&emu.cpu, v.button.flags, 0) catch unreachable; + } else { + controller.releaseButtons(&emu.cpu, v.button.flags, 0) catch unreachable; + } + }, + .key => { + controller.pressKey(&emu.cpu, v.key) catch unreachable; + }, + } + } + } + + fn button() void { + const btn = kos.getButton(); + if (btn >> 8 == 1) + emu.exit(); + } + + fn mouse() void { + const dev = &emu.sys.mouse_device; + const pos = kos.mouseGetWindowPosition(); + const events = kos.mouseGetEvents(); + const mouse_pressed: varvara.mouse.ButtonFlags = .{ + .left = events.left_pressed, + .middle = events.middle_pressed, + .right = events.right_pressed, + ._unused = 0, + }; + const mouse_released: varvara.mouse.ButtonFlags = .{ + .left = events.left_released, + .middle = events.middle_released, + .right = events.right_released, + ._unused = 0, + }; + if (emu.hide_cursor and pos.y > emu.screen_height) { + _ = kos.setCursor(0); + emu.hide_cursor = false; + } + if (!emu.hide_cursor and pos.y < emu.screen_height) { + _ = kos.setCursor(emu.null_cursor); + emu.hide_cursor = true; + } + dev.updatePosition(&emu.cpu, pos.x / emu.scale, pos.y / emu.scale) catch unreachable; + if (@as(u8, @bitCast(mouse_pressed)) > 0) { + dev.pressButtons(&emu.cpu, mouse_pressed) catch unreachable; + } + if (@as(u8, @bitCast(mouse_released)) > 0) { + dev.releaseButtons(&emu.cpu, mouse_released) catch unreachable; + } + } + }; + + callbacks.redraw(); + while (true) { + while (true) { + const event = kos.checkEvent(); + switch (event) { + .none => break, + .redraw => callbacks.redraw(), + .key => callbacks.key(), + .button => callbacks.button(), + .mouse => callbacks.mouse(), + else => {}, + } + } + const tick = kos.systemGetTimeCount(); + counter += (tick - last_tick) * 3; + last_tick = tick; + + if (counter > 5) { + counter -= 5; + emu.update() catch unreachable; + } else { + kos.sleep(1); + } + } +} diff --git a/programs/emulator/uxn/uxn b/programs/emulator/uxn/uxn new file mode 100755 index 0000000000000000000000000000000000000000..4911203d0d244fb2cd6b30a9a51f079a33f13ea3 GIT binary patch literal 59569 zcmeIb4SZ8Y)<1rerjTOF4Op;Z!GckXqT7@fS}54^qM!v)ilP=p#cWkpk*0_%YA}iB zHq=GM^<~+86nu7hTzAoDwFN{-T5Kr@6i{qH-hJsMhy~nIUTpv0Gjs1v(xy;R*yr>A zSx9p4%$YN1&YU@O=FFLU$BiC8X>@+7RVN6-gb{-9e!U<(q1Um`>_bSz-=|@JlccXF z-sb$O!Fe#%JKE?iHdT+ZnDqd{Zz2KXH-Ua7yCq2)Wl5IuEtgk~vRH)D0*iUs^~l+f zK3oNR+fdwu!UYy%X}-l6G*N;92}u@dpI7g_)gaws zkaVRFG~IpIl*;+@=Mz1*gPp7&DkVM4YNea@7OCkj&~{AtF9eGg}Eh`)0W zIzW?HHrm)Y+N2X*lR#o)?GjOR*TztK?sVTCgIMM(S*iaWlM^Rg%dp|9s8g0 z6j+kz!r@q-U~|r~m<2EZ$Ue~9Sj8>U980oTKF4ANq|vVTob>F5Xt0`rB*$yFB=gAQH+5Mk>+NOYlp zkG|B9;|Zkr#WKHh&ocpodp7!Qd!D6GEFWdb@zf zy*mTD_xweCZa?tV4f_DOfkSA6Sw6~G>~roBOJ_7YoApIw1G(nF97_TrO^6pTGzjgr zf$5fOcNAEr<2S+Tb3BnQ*bP81T`U_r-M1IrP-FA^j5f*DWh!Wvuf>FV*?$jp!W8N(;nNw+8YpdAsuHZVta213ZR6lBIrxAGxut zbMq~ub{$6g1m}K~EwH2qMzK=#tHF0C_)!E2(KJFLaASF*6EP1W6JV^vI0;NHlxAc~ zv$Lynav&{KBrz~~RABNLTuly4&cn}yz~l+|$q!7%|H-&Y3QR8W=7F_irdH)l(*={g zxhAw>1_qs-knEmrnNgSFpcdRwSe2U^qp1K@zc$Q0m<$F=TfzH8sm?Gz+b6!{>k_*X z^TiTfajrLh_UD4|)*O7T5FAi@#fFE37Be(#>3q6hHW1ZFyFLOAVR>yP%>vTV#mi8- zis8_aW|0EC=SyccLik1JM*v;|U9KY-zah=xZxqW8w^&}; zQ%wM_LG=d&fvX|0+;Q7?5K9aLvD_CE+k~8Im&E~$MtpGT!Vmr7(!;Ola$e|izH%wK zEU)0Q%4JzVDWwZ74*V!nS!AuYYR3fBiqX*t=PX)I*HHkKrU|`T4GuYHmySzXQTS~kCMvE(UhA)v9fovC2_L27i^pr1P) zB*<=HbsPu!Ik*7g6d)#Eeu~*tEUSbRJAFSon@r;Ky%~UeHUPx3*gf?M3u~b#6QQDe zl2%pt2$?ZKwMh#tAD|IZrGLL6u99GwdS|?Dl8*ajleE^q#}KzxET7kt&jNmQ!)6R< zv;Tle+U(zJh}-Pxy&rWg`U+p3_^QmH=O+C}vW8qO;fMH9Pi@bs1`TOpp~kv20%*DN{^h9#(Av7q7x zsfm7k=n}DPopi*%&!9Wv>2Z+2E9j=e)8o6avWIXjuNwflGWh2M4BetT;mu~XD56c3Nh_H=BZKP19SOQvE=^{| zs2^&ic}&iw1s0O=)5T`;n(1Gol$j zvE0L(!6d*N&ziALEMHc@=ppN%MYjo1Dt(S+laQkm%l)Ez0I3{^1FEB1V)4Oc9e(If zBZ6Ii$rFWAKFZOmJxDLPK$4`iV z_BYlV*ODqYbQNKX{hG$?9)n|loUsz45xO~1ESsmh8#FAt4iz&!xgkeh&A=H}1BwJM zP$;zH216im!fo|;Hny}t&B_Y^3XV8X8d&JkgCF|CCCLE&fh(lpAQi0V7o0}3v7Y}E zP?4q1dVV2p+V(u}8rg+Rzie)+yCO7{XYMWNT)DnKae6m8calQELE^yiDJLPPG z+FwVx<*|LhMifrQPe{E{qsgveCdgX=grO#88$?$X>n5hxnqo!w7-SKwSoZ8>PzF3q zu`x9X3x>>2*#a2P0n)YKVZ#4J(Y3AUC4DYu669W(fWCu(T%+mWFx~5!JCPu}0@7b^ zj;639fb$7>ZwlzAr9& szkPL6!q@IW3lgvCC;`L1Q16Y?nb>G| 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 literal 0 HcmV?d00001 -- 2.49.1