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/LICENSES/MPL-2.0.txt b/programs/emulator/uxn/LICENSES/MPL-2.0.txt new file mode 100644 index 000000000..a612ad981 --- /dev/null +++ b/programs/emulator/uxn/LICENSES/MPL-2.0.txt @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/programs/emulator/uxn/README b/programs/emulator/uxn/README new file mode 100644 index 000000000..42605b828 --- /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 device, 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..58d1bf1bf --- /dev/null +++ b/programs/emulator/uxn/build.zig.zon @@ -0,0 +1,22 @@ +// 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", + "LICENSE", + }, +} diff --git a/programs/emulator/uxn/src/kolibri.zig b/programs/emulator/uxn/src/kolibri.zig new file mode 100644 index 000000000..6e861c241 --- /dev/null +++ b/programs/emulator/uxn/src/kolibri.zig @@ -0,0 +1,522 @@ +// 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)); +} + +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 info: packed struct(u200) { + subfn: u32 = 0, + offset: u64, + size: u32, + buffer: *u8, + path0: u8 = 0, + pathptr: *const u8, + } = .{ + .offset = context.offset, + .size = buffer.len, + .buffer = @ptrCast(buffer), + .pathptr = @ptrCast(context.pathname), + }; + + const err = asm volatile ("int $0x40" + : [ret] "={eax}" (-> u32), + : [number] "{eax}" (SYS.file), + [info] "{ebx}" (&info), + ); + const size = asm volatile ("" + : [ret] "={ebx}" (-> u32), + ); + context.offset += size; + return switch (err) { + 0 => size, + 10 => error.AccessDenied, + 6 => size, + else => error.Unexpected, + }; + } + }.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..169fe37d0 --- /dev/null +++ b/programs/emulator/uxn/src/main.zig @@ -0,0 +1,361 @@ +// 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 file = kos.File{ .pathname = rompath }; + emu.rom = try uxn.loadRom(allocator, 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 { + fn bcd8(x: u8) u8 { + return (x & 0xf) + 10 * ((x & 0xf0) >> 4); + } + + 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; + // TODO: file device + }, + 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 => brk: { + const scancode2 = kos.getKey().key; + break :brk 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 => brk: { + emu.changeScale(); + break :brk 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 => brk: { + if (scancode1 > 0x40) { + break :brk null; + } + const ctrls = kos.getControlKeys(); + const k = if (ctrls.left_shift or ctrls.right_shift) symtab[scancode1 + 0x40] else symtab[scancode1]; + break :brk 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); + } + } +}