├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── flake.lock ├── flake.nix └── src ├── Debug.zig ├── lib ├── asm │ ├── assembler.zig │ ├── lib.zig │ └── scanner.zig ├── uxn │ ├── Cpu.zig │ ├── cpu │ │ ├── Stack.zig │ │ └── isa.zig │ └── lib.zig └── varvara │ ├── devices │ ├── audio.zig │ ├── audio │ │ ├── Envelope.zig │ │ └── Sample.zig │ ├── console.zig │ ├── controller.zig │ ├── datetime.zig │ ├── file.zig │ ├── fs │ │ ├── default.zig │ │ └── noop.zig │ ├── impl.zig │ ├── mouse.zig │ ├── screen.zig │ └── system.zig │ └── lib.zig ├── main.zig ├── shared.zig ├── uxn-asm └── main.zig ├── uxn-cli └── main.zig └── uxn-sdl └── main.zig /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A mostly complete implementation of most of the components of a complete [Uxntal](https://wiki.xxiivv.com/site/uxntal.html) and [Varvara](https://wiki.xxiivv.com/site/varvara.html) system in a library-first fashion. 2 | 3 | `zig build` will compile three binary analogues to the reference implementations of uxnasm, uxncli and uxnemu: 4 | 5 | - uxn-asm: The Uxntal assembler 6 | - uxn-cli: Runs Uxn ROMs in a headless fashion 7 | - uxn-sdl: Runs Uxn ROMs with audio and video support in SDL 8 | 9 | The tools support different arguments but are pretty basic. (see `--help` for each) 10 | 11 | If compiled with `-Denable_jit_assembly`, uxn-cli and uxn-sdl will accept assemble passed .tal files and run the result 12 | directly, much like a bytecode interpreter. 13 | 14 | All three major components (Uxn core VM, Varvara devices and assembler) are exposed as Zig modules for embedding in other environments. The modules are named as such: 15 | 16 | - `uxn-core` 17 | - `uxn-varvara` 18 | - `uxn-asm` 19 | 20 | The Varvara audio, video and input devices are independant of external systems for audio and video and can be implemented in a way that makes sense for the embedded application. Generic device intercepts for input and output can be defined in addition to or as replacement for any given device. 21 | 22 | Until a proper API documentation is available, the three produced binaries shall serve as usage examples. -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | // Although this function looks imperative, note that its job is to 4 | // declaratively construct a build graph that will be executed by an external 5 | // runner. 6 | pub fn build(b: *std.Build) void { 7 | // Standard target options allows the person running `zig build` to choose 8 | // what target to build for. Here we do not override the defaults, which 9 | // means any target is allowed, and the default is native. Other options 10 | // for restricting supported target set are available. 11 | const target = b.standardTargetOptions(.{}); 12 | 13 | // Standard optimization options allow the person running `zig build` to select 14 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not 15 | // set a preferred release mode, allowing the user to decide how to optimize. 16 | const optimize = b.standardOptimizeOption(.{}); 17 | 18 | const dep_clap = b.dependency("clap", .{ 19 | .target = target, 20 | .optimize = optimize, 21 | }); 22 | 23 | // Core library modules 24 | const core_mod = b.addModule( 25 | "uxn-core", 26 | .{ 27 | .root_source_file = b.path("src/lib/uxn/lib.zig"), 28 | }, 29 | ); 30 | 31 | const varvara_mod = b.addModule( 32 | "uxn-varvara", 33 | .{ 34 | .root_source_file = b.path("src/lib/varvara/lib.zig"), 35 | .imports = &.{.{ 36 | .name = "uxn-core", 37 | .module = core_mod, 38 | }}, 39 | }, 40 | ); 41 | 42 | const asm_mod = b.addModule( 43 | "uxn-asm", 44 | .{ 45 | .root_source_file = b.path("src/lib/asm/lib.zig"), 46 | .imports = &.{.{ 47 | .name = "uxn-core", 48 | .module = core_mod, 49 | }}, 50 | }, 51 | ); 52 | 53 | // Utility programs based on core libraries 54 | const build_options = b.addOptions(); 55 | const enable_jit_assembly = b.option( 56 | bool, 57 | "enable_jit_assembly", 58 | \\Enable just in time assembly of Uxntal (increases program size) 59 | , 60 | ) orelse false; 61 | 62 | build_options.addOption( 63 | bool, 64 | "enable_jit_assembly", 65 | enable_jit_assembly, 66 | ); 67 | 68 | const build_options_mod = build_options.createModule(); 69 | 70 | const shared_mod = b.addModule( 71 | "uxn-shared", 72 | .{ 73 | .root_source_file = b.path("src/shared.zig"), 74 | .imports = &.{ .{ 75 | .name = "uxn-core", 76 | .module = core_mod, 77 | }, .{ 78 | .name = "uxn-asm", 79 | .module = asm_mod, 80 | }, .{ 81 | .name = "clap", 82 | .module = dep_clap.module("clap"), 83 | }, .{ 84 | .name = "build_options", 85 | .module = build_options_mod, 86 | } }, 87 | }, 88 | ); 89 | 90 | const uxn_cli = b.addExecutable(.{ 91 | .name = "uxn-cli", 92 | .root_source_file = b.path("src/uxn-cli/main.zig"), 93 | .target = target, 94 | .optimize = optimize, 95 | }); 96 | 97 | uxn_cli.root_module.addImport("uxn-shared", shared_mod); 98 | uxn_cli.root_module.addImport("uxn-core", core_mod); 99 | uxn_cli.root_module.addImport("uxn-varvara", varvara_mod); 100 | uxn_cli.root_module.addImport("clap", dep_clap.module("clap")); 101 | uxn_cli.root_module.addImport("build_options", build_options_mod); 102 | uxn_cli.linkLibC(); 103 | 104 | if (enable_jit_assembly) 105 | uxn_cli.root_module.addImport("uxn-asm", asm_mod); 106 | 107 | if (target.result.cpu.arch != .wasm32) { 108 | const uxn_sdl = b.addExecutable(.{ 109 | .name = "uxn-sdl", 110 | .root_source_file = b.path("src/uxn-sdl/main.zig"), 111 | .target = target, 112 | .optimize = optimize, 113 | }); 114 | 115 | uxn_sdl.root_module.addImport("uxn-shared", shared_mod); 116 | uxn_sdl.root_module.addImport("uxn-core", core_mod); 117 | uxn_sdl.root_module.addImport("uxn-varvara", varvara_mod); 118 | uxn_sdl.root_module.addImport("clap", dep_clap.module("clap")); 119 | uxn_sdl.root_module.addImport("build_options", build_options_mod); 120 | uxn_sdl.linkLibC(); 121 | uxn_sdl.linkSystemLibrary("SDL2"); 122 | 123 | if (enable_jit_assembly) 124 | uxn_sdl.root_module.addImport("uxn-asm", asm_mod); 125 | 126 | b.installArtifact(uxn_sdl); 127 | 128 | const run_sdl_cmd = b.addRunArtifact(uxn_sdl); 129 | 130 | run_sdl_cmd.step.dependOn(b.getInstallStep()); 131 | 132 | if (b.args) |args| 133 | run_sdl_cmd.addArgs(args); 134 | 135 | const run_sdl_step = b.step("run-sdl", "Run the SDL evaluator"); 136 | run_sdl_step.dependOn(&run_sdl_cmd.step); 137 | } 138 | 139 | const uxn_asm = b.addExecutable(.{ 140 | .name = "uxn-asm", 141 | .root_source_file = b.path("src/uxn-asm/main.zig"), 142 | .target = target, 143 | .optimize = optimize, 144 | }); 145 | 146 | uxn_asm.root_module.addImport("uxn-asm", asm_mod); 147 | uxn_asm.root_module.addImport("clap", dep_clap.module("clap")); 148 | 149 | b.installArtifact(uxn_cli); 150 | b.installArtifact(uxn_asm); 151 | 152 | const run_cli_cmd = b.addRunArtifact(uxn_cli); 153 | const run_asm_cmd = b.addRunArtifact(uxn_asm); 154 | 155 | run_cli_cmd.step.dependOn(b.getInstallStep()); 156 | run_asm_cmd.step.dependOn(b.getInstallStep()); 157 | 158 | if (b.args) |args| { 159 | run_cli_cmd.addArgs(args); 160 | run_asm_cmd.addArgs(args); 161 | } 162 | 163 | const run_cli_step = b.step("run-cli", "Run the CLI evaluator"); 164 | run_cli_step.dependOn(&run_cli_cmd.step); 165 | 166 | const run_asm_step = b.step("run-asm", "Run the uxn assembler"); 167 | run_asm_step.dependOn(&run_asm_cmd.step); 168 | 169 | const unit_tests = b.addTest(.{ 170 | .root_source_file = b.path("src/main.zig"), 171 | .target = target, 172 | .optimize = optimize, 173 | }); 174 | 175 | const run_unit_tests = b.addRunArtifact(unit_tests); 176 | const test_step = b.step("test", "Run unit tests"); 177 | test_step.dependOn(&run_unit_tests.step); 178 | } 179 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .zuxn, 3 | .fingerprint = 0x3a108afea50e7a5e, 4 | 5 | .version = "0.0.1", 6 | 7 | .paths = .{"src/"}, 8 | 9 | .dependencies = .{ 10 | .clap = .{ 11 | .url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.10.0.tar.gz", 12 | .hash = "clap-0.10.0-oBajB434AQBDh-Ei3YtoKIRxZacVPF1iSwp3IX_ZB8f0", 13 | }, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "clap": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1741191590, 7 | "narHash": "sha256-leXnA97ITdvmBhD2YESLBZAKjBg+G4R/+PPPRslz/ec=", 8 | "type": "tarball", 9 | "url": "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.10.0.tar.gz" 10 | }, 11 | "original": { 12 | "type": "tarball", 13 | "url": "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.10.0.tar.gz" 14 | } 15 | }, 16 | "flake-compat": { 17 | "flake": false, 18 | "locked": { 19 | "lastModified": 1696426674, 20 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 21 | "owner": "edolstra", 22 | "repo": "flake-compat", 23 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 24 | "type": "github" 25 | }, 26 | "original": { 27 | "owner": "edolstra", 28 | "repo": "flake-compat", 29 | "type": "github" 30 | } 31 | }, 32 | "flake-compat_2": { 33 | "flake": false, 34 | "locked": { 35 | "lastModified": 1696426674, 36 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 37 | "owner": "edolstra", 38 | "repo": "flake-compat", 39 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 40 | "type": "github" 41 | }, 42 | "original": { 43 | "owner": "edolstra", 44 | "repo": "flake-compat", 45 | "type": "github" 46 | } 47 | }, 48 | "flake-utils": { 49 | "locked": { 50 | "lastModified": 1652776076, 51 | "narHash": "sha256-gzTw/v1vj4dOVbpBSJX4J0DwUR6LIyXo7/SuuTJp1kM=", 52 | "owner": "numtide", 53 | "repo": "flake-utils", 54 | "rev": "04c1b180862888302ddfb2e3ad9eaa63afc60cf8", 55 | "type": "github" 56 | }, 57 | "original": { 58 | "owner": "numtide", 59 | "ref": "v1.0.0", 60 | "repo": "flake-utils", 61 | "type": "github" 62 | } 63 | }, 64 | "flake-utils_2": { 65 | "inputs": { 66 | "systems": "systems" 67 | }, 68 | "locked": { 69 | "lastModified": 1705309234, 70 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 71 | "owner": "numtide", 72 | "repo": "flake-utils", 73 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 74 | "type": "github" 75 | }, 76 | "original": { 77 | "owner": "numtide", 78 | "repo": "flake-utils", 79 | "type": "github" 80 | } 81 | }, 82 | "flake-utils_3": { 83 | "inputs": { 84 | "systems": "systems_2" 85 | }, 86 | "locked": { 87 | "lastModified": 1705309234, 88 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 89 | "owner": "numtide", 90 | "repo": "flake-utils", 91 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 92 | "type": "github" 93 | }, 94 | "original": { 95 | "owner": "numtide", 96 | "repo": "flake-utils", 97 | "type": "github" 98 | } 99 | }, 100 | "gitignore": { 101 | "inputs": { 102 | "nixpkgs": [ 103 | "zls", 104 | "nixpkgs" 105 | ] 106 | }, 107 | "locked": { 108 | "lastModified": 1709087332, 109 | "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", 110 | "owner": "hercules-ci", 111 | "repo": "gitignore.nix", 112 | "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 113 | "type": "github" 114 | }, 115 | "original": { 116 | "owner": "hercules-ci", 117 | "repo": "gitignore.nix", 118 | "type": "github" 119 | } 120 | }, 121 | "nixpkgs": { 122 | "locked": { 123 | "lastModified": 0, 124 | "narHash": "sha256-cjbHI+zUzK5CPsQZqMhE3npTyYFt9tJ3+ohcfaOF/WM=", 125 | "path": "/nix/store/7g9h6nlrx5h1lwqy4ghxvbhb7imm3vcb-source", 126 | "type": "path" 127 | }, 128 | "original": { 129 | "id": "nixpkgs", 130 | "type": "indirect" 131 | } 132 | }, 133 | "root": { 134 | "inputs": { 135 | "clap": "clap", 136 | "flake-utils": "flake-utils", 137 | "nixpkgs": "nixpkgs", 138 | "zig-overlay": "zig-overlay", 139 | "zls": "zls" 140 | } 141 | }, 142 | "systems": { 143 | "locked": { 144 | "lastModified": 1681028828, 145 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 146 | "owner": "nix-systems", 147 | "repo": "default", 148 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 149 | "type": "github" 150 | }, 151 | "original": { 152 | "owner": "nix-systems", 153 | "repo": "default", 154 | "type": "github" 155 | } 156 | }, 157 | "systems_2": { 158 | "locked": { 159 | "lastModified": 1681028828, 160 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 161 | "owner": "nix-systems", 162 | "repo": "default", 163 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 164 | "type": "github" 165 | }, 166 | "original": { 167 | "owner": "nix-systems", 168 | "repo": "default", 169 | "type": "github" 170 | } 171 | }, 172 | "zig-overlay": { 173 | "inputs": { 174 | "flake-compat": "flake-compat", 175 | "flake-utils": "flake-utils_2", 176 | "nixpkgs": [ 177 | "nixpkgs" 178 | ] 179 | }, 180 | "locked": { 181 | "lastModified": 1741781532, 182 | "narHash": "sha256-CmNengGTJsX/Ae9WONnazUFzjgihejci4ilGSbj5/Rg=", 183 | "owner": "mitchellh", 184 | "repo": "zig-overlay", 185 | "rev": "773c6313d94baed1195f15aefb209060d76f6355", 186 | "type": "github" 187 | }, 188 | "original": { 189 | "owner": "mitchellh", 190 | "repo": "zig-overlay", 191 | "type": "github" 192 | } 193 | }, 194 | "zig-overlay_2": { 195 | "inputs": { 196 | "flake-compat": "flake-compat_2", 197 | "flake-utils": "flake-utils_3", 198 | "nixpkgs": [ 199 | "zls", 200 | "nixpkgs" 201 | ] 202 | }, 203 | "locked": { 204 | "lastModified": 1741263138, 205 | "narHash": "sha256-qlX8tgtZMTSOEeAM8AmC7K6mixgYOguhl/xLj5xQrXc=", 206 | "owner": "mitchellh", 207 | "repo": "zig-overlay", 208 | "rev": "627055069ee1409e8c9be7bcc533e8823fb87b18", 209 | "type": "github" 210 | }, 211 | "original": { 212 | "owner": "mitchellh", 213 | "repo": "zig-overlay", 214 | "type": "github" 215 | } 216 | }, 217 | "zls": { 218 | "inputs": { 219 | "gitignore": "gitignore", 220 | "nixpkgs": [ 221 | "nixpkgs" 222 | ], 223 | "zig-overlay": "zig-overlay_2" 224 | }, 225 | "locked": { 226 | "lastModified": 1741303397, 227 | "narHash": "sha256-A5Mn+mfIefOsX+eNBRHrDVkqFDVrD3iXDNsUL4TPhKo=", 228 | "owner": "zigtools", 229 | "repo": "zls", 230 | "rev": "7485feeeda45d1ad09422ae83af73307ab9e6c9e", 231 | "type": "github" 232 | }, 233 | "original": { 234 | "owner": "zigtools", 235 | "ref": "0.14.0", 236 | "repo": "zls", 237 | "type": "github" 238 | } 239 | } 240 | }, 241 | "root": "root", 242 | "version": 7 243 | } 244 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | flake-utils.url = "github:numtide/flake-utils/v1.0.0"; 4 | 5 | zig-overlay.url = "github:mitchellh/zig-overlay"; 6 | zig-overlay.inputs.nixpkgs.follows = "nixpkgs"; 7 | 8 | zls.url = "github:zigtools/zls/0.14.0"; 9 | zls.inputs.nixpkgs.follows = "nixpkgs"; 10 | 11 | # build.zig.zon 12 | clap.url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.10.0.tar.gz"; 13 | clap.flake = false; 14 | }; 15 | 16 | outputs = { self, zig-overlay, zls, clap, flake-utils, nixpkgs, ... }: 17 | flake-utils.lib.eachSystem ["x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin"] (system: 18 | let 19 | pkgs = nixpkgs.legacyPackages.${system}; 20 | zig-ver = "0.14.0"; 21 | in { 22 | packages.default = pkgs.stdenv.mkDerivation { 23 | name = "zuxn"; 24 | version = "0.0"; 25 | 26 | src = ./.; 27 | 28 | nativeBuildInputs = with pkgs; [ 29 | zig-overlay.packages.${system}.${zig-ver} 30 | ]; 31 | 32 | buildInputs = with pkgs; [ 33 | SDL2.dev 34 | ]; 35 | 36 | buildPhase = '' 37 | mkdir -p $out 38 | mkdir -p .cache/p 39 | 40 | cp -r ${clap} .cache/p/clap-0.10.0-oBajB434AQBDh-Ei3YtoKIRxZacVPF1iSwp3IX_ZB8f0 41 | 42 | zig build --global-cache-dir $(pwd)/.cache -p $out 43 | ''; 44 | }; 45 | 46 | devShells.default = pkgs.mkShell { 47 | name = "zuxn-dev"; 48 | 49 | buildInputs = [ 50 | zig-overlay.packages.${system}.${zig-ver} 51 | zls.packages.${system}.default 52 | 53 | pkgs.SDL2.dev 54 | ]; 55 | }; 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /src/Debug.zig: -------------------------------------------------------------------------------- 1 | const Debug = @This(); 2 | 3 | const std = @import("std"); 4 | 5 | const uxn = @import("uxn-core"); 6 | 7 | const Allocator = std.mem.Allocator; 8 | const File = std.fs.File; 9 | 10 | const Symbol = struct { 11 | addr: u16, 12 | symbol: [0x40:0]u8, 13 | }; 14 | 15 | const Location = struct { 16 | closest: *const Symbol, 17 | offset: u16, 18 | }; 19 | 20 | allocator: Allocator, 21 | symbols: std.ArrayListUnmanaged(Symbol), 22 | 23 | fn cmpAddr(ctx: void, a: Symbol, b: Symbol) bool { 24 | _ = ctx; 25 | 26 | return a.addr < b.addr; 27 | } 28 | 29 | pub fn loadSymbols(alloc: Allocator, reader: anytype) !Debug { 30 | var symbol_list = std.ArrayListUnmanaged(Symbol).empty; 31 | 32 | errdefer symbol_list.deinit(alloc); 33 | 34 | return while (true) { 35 | var temp: Symbol = undefined; 36 | var fbs = std.io.FixedBufferStream([]u8){ 37 | .buffer = &temp.symbol, 38 | .pos = 0, 39 | }; 40 | 41 | temp.addr = reader.readInt(u16, .big) catch { 42 | std.mem.sort(Symbol, symbol_list.items, {}, cmpAddr); 43 | 44 | return .{ 45 | .allocator = alloc, 46 | .symbols = symbol_list, 47 | }; 48 | }; 49 | 50 | try reader.streamUntilDelimiter(fbs.writer(), 0x00, null); 51 | 52 | @memset(temp.symbol[@truncate(fbs.getPos() catch unreachable)..], 0x00); 53 | 54 | try symbol_list.append(alloc, temp); 55 | } else unreachable; 56 | } 57 | 58 | pub fn unload(debug: *Debug) void { 59 | debug.symbols.deinit(debug.allocator); 60 | } 61 | 62 | pub fn locateSymbol(debug: *const Debug, addr: u16, allow_negative: bool) ?Location { 63 | var left: usize = 0; 64 | var right: usize = debug.symbols.items.len; 65 | var nearest_smaller: usize = 0; 66 | var nearest_bigger: usize = 0; 67 | 68 | const pos: ?usize = while (left < right) { 69 | const mid = left + (right - left) / 2; 70 | 71 | switch (std.math.order(addr, debug.symbols.items[mid].addr)) { 72 | .eq => break mid, 73 | .gt => { 74 | left = mid + 1; 75 | nearest_smaller = mid; 76 | }, 77 | .lt => { 78 | right = mid; 79 | nearest_bigger = mid; 80 | }, 81 | } 82 | } else null; 83 | 84 | if (pos) |direct_match| { 85 | return .{ 86 | .closest = &debug.symbols.items[direct_match], 87 | .offset = 0, 88 | }; 89 | } else { 90 | const b = &debug.symbols.items[nearest_bigger]; 91 | const s = &debug.symbols.items[nearest_smaller]; 92 | 93 | return if (allow_negative) 94 | if ((b.addr - addr) > (addr - s.addr)) .{ 95 | .closest = s, 96 | .offset = addr - s.addr, 97 | } else .{ 98 | .closest = b, 99 | .offset = b.addr - addr, 100 | } 101 | else 102 | .{ 103 | .closest = s, 104 | .offset = addr - s.addr, 105 | }; 106 | } 107 | } 108 | 109 | fn opcodeColor(i: uxn.Cpu.Opcode) struct { u8, u8, u8 } { 110 | return switch (i.baseOpcode()) { 111 | .BRK => switch (i) { 112 | .BRK => .{ 0xff, 0x00, 0x00 }, 113 | .LIT, .LIT2, .LITr, .LIT2r => .{ 66, 135, 245 }, 114 | .JCI, .JMI, .JSI => .{ 105, 66, 245 }, 115 | else => unreachable, 116 | }, 117 | .JMP, .JCN, .JSR => .{ 66, 245, 170 }, 118 | .POP, .NIP, .SWP, .ROT, .DUP, .OVR, .STH => .{ 245, 111, 66 }, 119 | .DEI, .DEO => .{ 245, 209, 66 }, 120 | .LDZ, .LDR, .LDA => .{ 66, 245, 90 }, 121 | .STZ, .STR, .STA => .{ 66, 203, 245 }, 122 | .INC, .ADD, .SUB, .MUL, .DIV => .{ 245, 66, 90 }, 123 | .EQU, .NEQ, .GTH, .LTH, .AND, .ORA, .EOR, .SFT => .{ 242, 17, 47 }, 124 | }; 125 | } 126 | 127 | fn dumpStack(stack: *const uxn.Cpu.Stack) void { 128 | const offset: u8 = 8; 129 | 130 | var i: u8 = stack.sp -% offset; 131 | 132 | // Print index row 133 | while (i != stack.sp +% offset + 1) : (i +%= 1) { 134 | if (i == stack.sp) { 135 | std.debug.print("\x1b[1;30m[{x:0>2}]\x1b[0m ", .{i}); 136 | } else { 137 | std.debug.print("\x1b[1;30m{x:0>2}\x1b[0m ", .{i}); 138 | } 139 | } 140 | 141 | std.debug.print("\n", .{}); 142 | 143 | i = stack.sp -% offset; 144 | 145 | // Print value row 146 | while (i != stack.sp +% offset + 1) : (i +%= 1) { 147 | if (i == stack.sp) { 148 | std.debug.print("\x1b[1;31m[{x:0>2}]\x1b[0m ", .{stack.data[i]}); 149 | } else { 150 | std.debug.print("{x:0>2} ", .{stack.data[i]}); 151 | } 152 | } 153 | 154 | std.debug.print("\n", .{}); 155 | } 156 | 157 | pub fn onDebugHook(cpu: *uxn.Cpu, data: ?*anyopaque) void { 158 | const debug_data: ?*const Debug = @alignCast(@ptrCast(data)); 159 | 160 | std.debug.print("Breakpoint triggered\n", .{}); 161 | 162 | var fallback = true; 163 | 164 | // Point at PC+1 because PC will always be the DEO and the next instruction 165 | // is more interesting. 166 | const pc = cpu.pc + 1; 167 | 168 | const color = opcodeColor(.fromByte(cpu.mem[pc])); 169 | const instr: uxn.Cpu.Opcode = .fromByte(cpu.mem[pc]); 170 | 171 | if (debug_data) |debug| { 172 | if (debug.locateSymbol(pc, false)) |stop_location| { 173 | fallback = false; 174 | std.debug.print("Next PC = {x:0>4} ({s}{c}#{x}): \x1b[38;2;{d};{d};{d}m{s}\x1b[0m\n", .{ 175 | pc, 176 | stop_location.closest.symbol, 177 | @as(u8, if (stop_location.closest.addr > pc) '-' else '+'), 178 | stop_location.offset, 179 | color[0], 180 | color[1], 181 | color[2], 182 | instr.mnemonic(), 183 | }); 184 | } 185 | } 186 | 187 | if (fallback) { 188 | std.debug.print("PC = {x:0>4}: \x1b[38;2;{d};{d};{d}m{s}\x1b[0m\n", .{ 189 | pc, 190 | color[0], 191 | color[1], 192 | color[2], 193 | instr.mnemonic(), 194 | }); 195 | } 196 | 197 | std.debug.print("\n", .{}); 198 | 199 | std.debug.print("Working Stack: \n", .{}); 200 | dumpStack(&cpu.wst); 201 | 202 | std.debug.print("\n", .{}); 203 | 204 | std.debug.print("Return Stack: \n", .{}); 205 | dumpStack(&cpu.rst); 206 | 207 | std.debug.print("\n", .{}); 208 | 209 | // How many locations should be displayed in one line 210 | const w: usize = 16; 211 | 212 | // Which "line" in a hexdump of the memory this PC is on 213 | const memory_line = pc / w; 214 | 215 | // How many lines of context should be displayed above and below 216 | const context = 4; 217 | 218 | // Print column offset header 219 | std.debug.print(" ", .{}); 220 | 221 | for (0..w) |coff| { 222 | if (pc % w == coff) { 223 | std.debug.print("{x:<2} v ", .{coff}); 224 | } else { 225 | std.debug.print("{x:<8}", .{coff}); 226 | } 227 | } 228 | 229 | std.debug.print("\n", .{}); 230 | 231 | var print_lits: usize = 0; 232 | 233 | // Print memory dump 234 | for (memory_line -| context..memory_line +| context) |page| { 235 | if (page == memory_line) 236 | std.debug.print("> ", .{}) 237 | else 238 | std.debug.print(" ", .{}); 239 | 240 | std.debug.print("{x:0>4} | ", .{page * w}); 241 | 242 | for (page * w..page * w + w) |addr| { 243 | if (print_lits > 0) { 244 | std.debug.print("{x:0>2} ", .{ 245 | cpu.mem[addr], 246 | }); 247 | 248 | print_lits -= 1; 249 | } else { 250 | const opcode = uxn.Cpu.Opcode.fromByte(cpu.mem[addr]); 251 | const r, const g, const b = opcodeColor(opcode); 252 | 253 | std.debug.print("\x1b[38;2;{d};{d};{d}m{s:<8}\x1b[0m", .{ 254 | r, 255 | g, 256 | b, 257 | opcode.mnemonic(), 258 | }); 259 | 260 | switch (opcode) { 261 | .LIT, .LITr => { 262 | print_lits = 1; 263 | }, 264 | 265 | .LIT2, .LIT2r, .JCI, .JMI, .JSI => { 266 | print_lits = 2; 267 | }, 268 | 269 | else => {}, 270 | } 271 | } 272 | } 273 | 274 | std.debug.print("\n", .{}); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/lib/asm/assembler.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | 4 | const scan = @import("scanner.zig"); 5 | 6 | const can_include = true; 7 | 8 | const fs = std.fs; 9 | const os = std.os; 10 | 11 | pub const AssemblerError = error{ 12 | OutOfMemory, 13 | 14 | TooManyMacros, 15 | TooManyReferences, 16 | TooManyLabels, 17 | TooManyNestedLambas, 18 | UnbalancedLambda, 19 | 20 | ReferenceOutOfBounds, 21 | LabelAlreadyDefined, 22 | 23 | MissingScopeLabel, 24 | 25 | UndefinedLabel, 26 | UndefinedMacro, 27 | InvalidMacroDefinition, 28 | MacroBodyTooLong, 29 | 30 | NotImplemented, 31 | 32 | IncludeNotFound, 33 | NotAllowed, 34 | CannotOpenFile, 35 | }; 36 | 37 | pub fn Assembler(comptime lim: scan.Limits) type { 38 | return struct { 39 | pub const Scanner = scan.Scanner(lim); 40 | 41 | pub const Span = struct { 42 | line: usize, 43 | column: usize, 44 | }; 45 | 46 | pub const LexicalInformation = struct { 47 | file: ?[]const u8, 48 | from: Span, 49 | to: Span, 50 | }; 51 | 52 | pub const DefinedLabel = struct { 53 | definition: ?LexicalInformation = null, 54 | 55 | label: Scanner.Label, 56 | addr: ?u16, 57 | references: std.ArrayListUnmanaged(Reference), 58 | }; 59 | 60 | pub const ReferenceType = union(enum) { 61 | zero: void, 62 | relative: u16, 63 | absolute: void, 64 | }; 65 | 66 | pub const Reference = struct { 67 | definition: ?LexicalInformation = null, 68 | 69 | addr: u16, 70 | offset: u16, 71 | type: Scanner.AddressType, 72 | }; 73 | 74 | pub const Macro = struct { 75 | name: Scanner.Label, 76 | body: std.ArrayListUnmanaged(Scanner.SourceToken), 77 | }; 78 | 79 | allocator: mem.Allocator, 80 | 81 | rom_length: usize = 0, 82 | 83 | default_input_filename: ?[]const u8 = null, 84 | include_base: ?fs.Dir, 85 | include_follow: bool = true, 86 | include_stack: std.ArrayListUnmanaged([]const u8) = .empty, 87 | 88 | last_root_label: ?Scanner.Label = null, 89 | 90 | err_input_pos: ?LexicalInformation = null, 91 | err_token: ?Scanner.SourceToken = null, 92 | 93 | labels: std.ArrayListUnmanaged(DefinedLabel) = .empty, 94 | macros: std.ArrayListUnmanaged(Macro) = .empty, 95 | lambdas: std.ArrayListUnmanaged(usize) = .empty, 96 | lambda_counter: usize = 0, 97 | 98 | pub fn init(alloc: mem.Allocator, include_base: ?fs.Dir) @This() { 99 | return .{ 100 | .allocator = alloc, 101 | .include_base = include_base, 102 | }; 103 | } 104 | 105 | pub fn deinit(assembler: *@This()) void { 106 | for (assembler.include_stack.items) |inc| assembler.allocator.free(inc); 107 | for (assembler.macros.items) |*macro| macro.body.deinit(assembler.allocator); 108 | for (assembler.labels.items) |*label| { 109 | if (label.definition) |def| 110 | if (def.file) |f| 111 | assembler.allocator.free(f); 112 | 113 | for (label.references.items) |ref| 114 | if (ref.definition) |def| 115 | if (def.file) |f| 116 | assembler.allocator.free(f); 117 | 118 | label.references.deinit(assembler.allocator); 119 | } 120 | 121 | assembler.include_stack.deinit(assembler.allocator); 122 | assembler.lambdas.deinit(assembler.allocator); 123 | assembler.macros.deinit(assembler.allocator); 124 | assembler.labels.deinit(assembler.allocator); 125 | 126 | if (assembler.err_input_pos) |err| 127 | if (err.file) |f| 128 | assembler.allocator.free(f); 129 | } 130 | 131 | fn lexicalInformationFromToken( 132 | assembler: *@This(), 133 | token: Scanner.SourceToken, 134 | ) LexicalInformation { 135 | const file = if (assembler.include_stack.getLastOrNull()) |last| 136 | assembler.allocator.dupe(u8, last) catch null 137 | else 138 | null; 139 | 140 | return .{ 141 | .file = file, 142 | .from = .{ 143 | .line = token.start[0], 144 | .column = token.start[1], 145 | }, 146 | .to = .{ 147 | .line = token.end[0], 148 | .column = token.end[1], 149 | }, 150 | }; 151 | } 152 | 153 | fn lexicalInformationFromScanner( 154 | assembler: *@This(), 155 | scanner: *const Scanner, 156 | ) LexicalInformation { 157 | const file = if (assembler.include_stack.getLastOrNull()) |last| 158 | assembler.allocator.dupe(u8, last) catch null 159 | else 160 | null; 161 | 162 | return .{ 163 | .file = file, 164 | .from = .{ 165 | .line = scanner.location[0], 166 | .column = scanner.location[1], 167 | }, 168 | .to = .{ 169 | .line = scanner.location[0], 170 | .column = scanner.location[1] + 1, 171 | }, 172 | }; 173 | } 174 | 175 | fn lookupLabel(assembler: *@This(), label: Scanner.Label) ?*DefinedLabel { 176 | for (assembler.labels.items) |*l| 177 | if (mem.eql(u8, &label, &l.label)) 178 | return l; 179 | 180 | return null; 181 | } 182 | 183 | fn retrieveLabel(assembler: *@This(), label: Scanner.TypedLabel) !*DefinedLabel { 184 | const full = try assembler.fullLabel(label); 185 | 186 | if (assembler.lookupLabel(full)) |def| { 187 | return def; 188 | } 189 | 190 | const definition = assembler.labels.addOne(assembler.allocator) catch return error.TooManyLabels; 191 | 192 | definition.* = .{ 193 | .definition = null, 194 | .label = full, 195 | .addr = null, 196 | .references = .empty, 197 | }; 198 | 199 | return definition; 200 | } 201 | 202 | fn defineLabel(assembler: *@This(), label: Scanner.TypedLabel, addr: u16) !*DefinedLabel { 203 | const definition = try assembler.retrieveLabel(label); 204 | 205 | if (definition.addr != null) 206 | return error.LabelAlreadyDefined; 207 | 208 | definition.*.addr = addr; 209 | 210 | return definition; 211 | } 212 | 213 | fn generateLambdaLabel(id: usize) Scanner.TypedLabel { 214 | var lambda_label = [1:0]u8{0x00} ** Scanner.limits.identifier_length; 215 | var stream = std.io.fixedBufferStream(&lambda_label); 216 | 217 | stream.writer().print("lambda/{x:0>3}", .{id}) catch unreachable; 218 | 219 | return .{ .root = lambda_label }; 220 | } 221 | 222 | fn fullLabel(assembler: *@This(), label: Scanner.TypedLabel) !Scanner.Label { 223 | switch (label) { 224 | .root => |l| { 225 | // Special case "smart" lambda labels that get a unique identifier whenever encountered. 226 | if (std.mem.eql(u8, "{", mem.sliceTo(&l, 0))) { 227 | const ll = generateLambdaLabel(assembler.lambda_counter).root; 228 | 229 | assembler.lambdas.append(assembler.allocator, assembler.lambda_counter) catch 230 | return error.TooManyNestedLambas; 231 | 232 | assembler.lambda_counter += 1; 233 | 234 | return ll; 235 | } else { 236 | return l; 237 | } 238 | }, 239 | 240 | .scoped => |s| { 241 | const parent = mem.sliceTo(&(assembler.last_root_label orelse return error.MissingScopeLabel), 0); 242 | const parent_local = mem.sliceTo(parent, '/'); 243 | const child = mem.sliceTo(&s, 0); 244 | 245 | var full: Scanner.Label = [1:0]u8{0x00} ** Scanner.limits.identifier_length; 246 | 247 | @memcpy(full[0..parent_local.len], parent_local); 248 | @memcpy(full[parent_local.len + 1 .. parent_local.len + 1 + child.len], child); 249 | 250 | full[parent_local.len] = '/'; 251 | 252 | return full; 253 | }, 254 | } 255 | } 256 | 257 | fn lookupOffset(assembler: *@This(), offset: Scanner.Offset) !?u16 { 258 | return switch (offset) { 259 | .literal => |lit| lit, 260 | .label => |lbl| if (assembler.lookupLabel(try assembler.fullLabel(lbl))) |l| l.addr else null, 261 | }; 262 | } 263 | 264 | fn rememberLocation( 265 | assembler: *@This(), 266 | reference: Scanner.Address, 267 | addr: u16, 268 | offset: u16, 269 | ) !*Reference { 270 | var definition = try assembler.retrieveLabel(reference.label); 271 | 272 | const ref = definition.references.addOne(assembler.allocator) catch 273 | return error.TooManyReferences; 274 | 275 | ref.* = .{ 276 | .addr = addr, 277 | .offset = offset, 278 | .type = reference.type, 279 | }; 280 | 281 | return ref; 282 | } 283 | 284 | fn AssembleError( 285 | comptime Reader: type, 286 | comptime Writer: type, 287 | comptime Seeker: type, 288 | ) type { 289 | return AssemblerError || 290 | Reader.Error || 291 | fs.File.Reader.Error || 292 | Writer.Error || 293 | Seeker.GetSeekPosError || 294 | Seeker.SeekError || 295 | Scanner.Error; 296 | } 297 | 298 | fn processToken( 299 | assembler: *@This(), 300 | scanner: *Scanner, 301 | token: Scanner.SourceToken, 302 | input: anytype, 303 | output: anytype, 304 | seekable: anytype, 305 | ) AssembleError(@TypeOf(input), @TypeOf(output), @TypeOf(seekable))!void { 306 | assembler.err_token = null; 307 | 308 | errdefer { 309 | assembler.err_token = token; 310 | } 311 | 312 | switch (token.token) { 313 | .literal, .raw_literal => |lit| { 314 | if (token.token != .raw_literal) 315 | if (lit == .byte) 316 | // LIT 317 | try output.writeByte(0x80) 318 | else 319 | // LIT2 320 | try output.writeByte(0xa0); 321 | 322 | switch (lit) { 323 | .byte => |b| try output.writeByte(b), 324 | .short => |s| try output.writeInt(u16, s, .big), 325 | } 326 | }, 327 | 328 | .label => |l| { 329 | var label_def = try assembler.defineLabel(l, @truncate(try seekable.getPos())); 330 | 331 | label_def.definition = assembler.lexicalInformationFromToken(token); 332 | 333 | if (l == .root) { 334 | assembler.last_root_label = l.root; 335 | } 336 | }, 337 | .address => |addr| { 338 | var ref = try assembler.rememberLocation(addr, @truncate(try seekable.getPos()), 0); 339 | 340 | ref.definition = assembler.lexicalInformationFromToken(token); 341 | 342 | try switch (addr.type) { 343 | .zero => output.writeInt(u16, 0x80aa, .big), 344 | .zero_raw => output.writeByte(0xaa), 345 | .relative => output.writeInt(u16, 0x80aa, .big), 346 | .relative_raw => output.writeByte(0xaa), 347 | .absolute => output.writeInt(u24, 0xa0aaaa, .big), 348 | .absolute_raw => output.writeInt(u16, 0xaaaa, .big), 349 | }; 350 | }, 351 | .padding => |pad| try switch (pad) { 352 | .absolute => |offset| seekable.seekTo(try assembler.lookupOffset(offset) orelse return error.UndefinedLabel), 353 | .relative => |offset| seekable.seekBy(try assembler.lookupOffset(offset) orelse return error.UndefinedLabel), 354 | }, 355 | .include => |path| if (can_include) { 356 | try assembler.includeFile( 357 | output, 358 | seekable, 359 | mem.sliceTo(&path, 0), 360 | ); 361 | } else { 362 | return error.NotImplemented; 363 | }, 364 | .instruction => |op| { 365 | try output.writeByte(op.encoded); 366 | }, 367 | .jci, .jmi, .jsi => |label| { 368 | const pos = try seekable.getPos(); 369 | const reference = Scanner.Address{ 370 | .type = .absolute, 371 | .label = label, 372 | }; 373 | 374 | var ref = try assembler.rememberLocation(reference, @truncate(pos), @truncate(pos + 3)); 375 | 376 | ref.definition = assembler.lexicalInformationFromToken(token); 377 | 378 | try output.writeByte(switch (token.token) { 379 | .jci => 0x20, 380 | .jmi => 0x40, 381 | else => 0x60, 382 | }); 383 | 384 | try output.writeInt(u16, 0xaaaa, .big); 385 | }, 386 | 387 | .word => |w| try output.writeAll(mem.sliceTo(&w, 0)), 388 | 389 | .macro_definition => |name| { 390 | const start = try scanner.readToken(input) orelse 391 | return error.InvalidMacroDefinition; 392 | 393 | // %MACRO { } gets scanned as: so we expect that here. 394 | if (start.token != .jsi and start.token.jsi != .root) { 395 | return error.InvalidMacroDefinition; 396 | } 397 | 398 | // Special case it a bit so that "{}" and "{ }" both work as empty body definitions 399 | const empty_body = if (mem.eql(u8, "{", mem.sliceTo(&start.token.jsi.root, 0))) 400 | false 401 | else if (mem.eql(u8, "{}", mem.sliceTo(&start.token.jsi.root, 0))) 402 | true 403 | else 404 | return error.InvalidMacroDefinition; 405 | 406 | var body = std.ArrayListUnmanaged(Scanner.SourceToken).empty; 407 | 408 | if (!empty_body) { 409 | while (try scanner.readToken(input)) |tok| { 410 | if (tok.token == .curly_close) 411 | break; 412 | 413 | body.append(assembler.allocator, tok) catch return error.MacroBodyTooLong; 414 | } 415 | } 416 | 417 | assembler.macros.append(assembler.allocator, .{ 418 | .name = name, 419 | .body = body, 420 | }) catch return error.TooManyMacros; 421 | }, 422 | .macro_expansion => |name| { 423 | const macro = for (assembler.macros.items) |macro| { 424 | if (mem.eql(u8, ¯o.name, &name)) 425 | break macro; 426 | } else return error.UndefinedMacro; 427 | 428 | // XXX: A macro can include itself and murder our stack. Introduce a max evaluation depth. 429 | for (macro.body.items) |macro_token| 430 | try assembler.processToken(scanner, macro_token, input, output, seekable); 431 | }, 432 | 433 | .curly_close => { 434 | const lambda = assembler.lambdas.pop() orelse return error.UnbalancedLambda; 435 | const label = generateLambdaLabel(lambda); 436 | 437 | var label_def = try assembler.defineLabel(label, @truncate(try seekable.getPos())); 438 | 439 | label_def.definition = assembler.lexicalInformationFromToken(token); 440 | }, 441 | } 442 | } 443 | 444 | pub fn assemble( 445 | assembler: *@This(), 446 | input: anytype, 447 | output: anytype, 448 | seekable: anytype, 449 | ) AssembleError(@TypeOf(input), @TypeOf(output), @TypeOf(seekable))!void { 450 | var scanner = Scanner.init(); 451 | 452 | errdefer { 453 | assembler.err_input_pos = assembler.lexicalInformationFromScanner(&scanner); 454 | } 455 | 456 | while (try scanner.readToken(input)) |token| { 457 | try assembler.processToken(&scanner, token, input, output, seekable); 458 | } 459 | 460 | // N.B. the reference assembler only tracks writes for the rom length, 461 | // while this one includes pads. A pad without writes at the end 462 | // of the source will include 0x00 bytes in the output while the 463 | // reference will implicitely fill in those 0x00 when loading the rom. 464 | assembler.rom_length = @truncate(try seekable.getPos()); 465 | 466 | try assembler.resolveReferences(output, seekable); 467 | } 468 | 469 | pub fn includeFile( 470 | assembler: *@This(), 471 | output: anytype, 472 | seekable: anytype, 473 | path: []const u8, 474 | ) !void { 475 | const dir = assembler.include_base orelse 476 | return error.NotAllowed; 477 | 478 | var full_path_buffer: [fs.max_path_bytes]u8 = undefined; 479 | 480 | // Determine canonical path to included file 481 | const builtin = @import("builtin"); 482 | 483 | const full_path = if (builtin.target.cpu.arch != .wasm32) 484 | dir.realpath(path, &full_path_buffer) catch return error.IncludeNotFound 485 | else 486 | path; 487 | 488 | // Open the include file 489 | const file = dir.openFile(full_path, .{}) catch 490 | return error.CannotOpenFile; 491 | 492 | defer file.close(); 493 | 494 | // If we're following relative includes, update our include_base to point 495 | // to the parent folder of the included file and set it back to our old value 496 | // once finished with that file. 497 | if (assembler.include_follow) { 498 | const basename = fs.path.dirname(full_path) orelse 499 | return error.IncludeNotFound; 500 | 501 | assembler.include_base = dir.openDir(basename, .{}) catch 502 | return error.IncludeNotFound; 503 | } 504 | 505 | defer if (assembler.include_follow) { 506 | assembler.include_base.?.close(); 507 | assembler.include_base = dir; 508 | }; 509 | 510 | try assembler.include_stack.ensureUnusedCapacity(assembler.allocator, 1); 511 | 512 | const included_path = try assembler.allocator.dupe(u8, full_path); 513 | 514 | assembler.include_stack.appendAssumeCapacity(included_path); 515 | 516 | // Do assemble 517 | const reader = file.reader(); 518 | var scanner = Scanner.init(); 519 | 520 | errdefer { 521 | assembler.err_input_pos = assembler.lexicalInformationFromScanner(&scanner); 522 | } 523 | 524 | while (try scanner.readToken(reader)) |token| { 525 | try assembler.processToken( 526 | &scanner, 527 | token, 528 | reader, 529 | output, 530 | seekable, 531 | ); 532 | } 533 | 534 | // We don't defer this so our include stack remains valid and pointed 535 | // at the failing file if the loop fails 536 | assembler.allocator.free(assembler.include_stack.pop().?); 537 | } 538 | 539 | fn resolveReferences( 540 | assembler: *@This(), 541 | output: anytype, 542 | seekable: anytype, 543 | ) !void { 544 | for (assembler.labels.items) |label| { 545 | if (label.addr) |addr| { 546 | if (label.references.items.len == 0) { 547 | // TODO: issue diagnostic for unused label 548 | } else { 549 | for (label.references.items) |ref| { 550 | const ref_pos = switch (ref.type) { 551 | // Literal references start after the LIT/LIT2/JSI/JMI/JCI opcode 552 | .zero, .relative, .absolute => ref.addr + 1, 553 | 554 | // Raw references start wherever they start. 555 | .zero_raw, .relative_raw, .absolute_raw => ref.addr, 556 | }; 557 | 558 | // Seek to the reference position and replace our placeholder 0xaa... with the 559 | // resolved reference. 560 | try seekable.seekTo(ref_pos); 561 | 562 | switch (ref.type) { 563 | .zero, .zero_raw => { 564 | try output.writeByte(@truncate(addr)); 565 | }, 566 | 567 | .absolute, .absolute_raw => { 568 | try output.writeInt(u16, addr -% ref.offset, .big); 569 | }, 570 | 571 | .relative, .relative_raw => { 572 | const target_addr: i16 = @as(i16, @bitCast(addr -% ref.offset)); 573 | 574 | const signed_pc: i16 = @intCast(ref_pos); 575 | const relative = target_addr - signed_pc - 2; 576 | 577 | if (relative > 127 or relative < -128) 578 | return error.ReferenceOutOfBounds; 579 | 580 | try output.writeInt(i8, @as(i8, @truncate(relative)), .big); 581 | }, 582 | } 583 | } 584 | } 585 | } else if (label.references.items.len > 0) { 586 | return error.UndefinedLabel; 587 | } 588 | } 589 | } 590 | 591 | fn sortLabel(_: void, a: DefinedLabel, b: DefinedLabel) bool { 592 | return a.addr.? < b.addr.?; 593 | } 594 | 595 | pub fn generateSymbols( 596 | assembler: *@This(), 597 | output: anytype, 598 | ) @TypeOf(output).Error!void { 599 | // Sort the symbols before writing them so that inside the .sym file, 600 | // they are sorted from lowest to highest address. Reference assembler 601 | // gets away without sorting because label definitions and references 602 | // are kept in two separate lists, but since reference lookups also end 603 | // up in .labels here, with a null address, the list can be out of order. 604 | std.mem.sort(DefinedLabel, assembler.labels.items, {}, sortLabel); 605 | 606 | for (assembler.labels.items) |label| { 607 | if (label.addr) |addr| { 608 | try output.writeInt(u16, addr, .big); 609 | try output.print("{s}\x00", .{mem.sliceTo(&label.label, 0)}); 610 | } 611 | } 612 | } 613 | 614 | pub fn issueDiagnostic(assembler: *@This(), err: anyerror, output: anytype) !void { 615 | const default_input = assembler.default_input_filename orelse ""; 616 | 617 | const error_str = switch (err) { 618 | error.OutOfMemory => "Out of memory", 619 | error.TooManyMacros => "Macro limit exceeded", 620 | error.TooManyReferences => "Reference limit exceeded", 621 | error.TooManyLabels => "Label limit exceeded", 622 | error.TooManyNestedLambas => "Nested lambda depth exceeded", 623 | error.UnbalancedLambda => "Unbalanced lambda paranetheses", 624 | error.ReferenceOutOfBounds => "Relative reference distance too far from definition", 625 | error.LabelAlreadyDefined => "Label already defined", 626 | error.MissingScopeLabel => "Missing parent label", 627 | error.UndefinedLabel => "Undefined label found where definition is required", 628 | error.UndefinedMacro => "Undefined macro name", 629 | error.InvalidMacroDefinition => "Malformed macro definition", 630 | error.MacroBodyTooLong => "Macro body limit exceeded", 631 | error.NotImplemented => "Not implemented yet", 632 | error.IncludeNotFound => "Include file not found", 633 | error.NotAllowed => "Function disabled", 634 | error.CannotOpenFile => "Cannot open file", 635 | 636 | else => @errorName(err), 637 | }; 638 | 639 | if (err == error.UndefinedLabel) { 640 | try output.print("{s}: undefined labels found:\n", .{ 641 | assembler.include_stack.getLastOrNull() orelse default_input, 642 | }); 643 | 644 | for (assembler.labels.items) |label| { 645 | if (label.addr == null and label.references.items.len > 0) { 646 | const first_ref = label.references.items[0]; 647 | 648 | try output.print("{s}: @{s} (first referenced: {s}:{}:{})\n", .{ 649 | assembler.include_stack.getLastOrNull() orelse default_input, 650 | label.label, 651 | first_ref.definition.?.file orelse default_input, 652 | first_ref.definition.?.from.line, 653 | first_ref.definition.?.from.column, 654 | }); 655 | } 656 | } 657 | } else if (assembler.err_token) |token_err| { 658 | const location = token_err.start; 659 | 660 | if (token_err.token == .label and err == error.LabelAlreadyDefined) { 661 | const first_ref = assembler.retrieveLabel(token_err.token.label) catch { 662 | try output.print("{s}:{}:{}: {s}\n", .{ 663 | assembler.include_stack.getLastOrNull() orelse default_input, 664 | location[0], 665 | location[1], 666 | error_str, 667 | }); 668 | 669 | return; 670 | }; 671 | 672 | try output.print("{s}:{}:{}: {s} (first defined: {s}:{}:{})\n", .{ 673 | assembler.include_stack.getLastOrNull() orelse default_input, 674 | location[0], 675 | location[1], 676 | error_str, 677 | first_ref.definition.?.file orelse default_input, 678 | first_ref.definition.?.from.line, 679 | first_ref.definition.?.from.column, 680 | }); 681 | } else { 682 | try output.print("{s}:{}:{}: {s}\n", .{ 683 | assembler.include_stack.getLastOrNull() orelse default_input, 684 | location[0], 685 | location[1], 686 | error_str, 687 | }); 688 | } 689 | } else if (assembler.err_input_pos) |lexer_err_pos| { 690 | try output.print("{s}:{}:{}: {s}\n", .{ 691 | lexer_err_pos.file orelse default_input, 692 | lexer_err_pos.from.line, 693 | lexer_err_pos.from.column, 694 | error_str, 695 | }); 696 | } else { 697 | try output.print("{s}: {s}\n", .{ 698 | assembler.include_stack.getLastOrNull() orelse default_input, 699 | error_str, 700 | }); 701 | } 702 | } 703 | }; 704 | } 705 | -------------------------------------------------------------------------------- /src/lib/asm/lib.zig: -------------------------------------------------------------------------------- 1 | pub const Assembler = @import("assembler.zig").Assembler; 2 | -------------------------------------------------------------------------------- /src/lib/asm/scanner.zig: -------------------------------------------------------------------------------- 1 | const uxn = @import("uxn-core"); 2 | 3 | const std = @import("std"); 4 | const mem = std.mem; 5 | const fmt = std.fmt; 6 | const ascii = std.ascii; 7 | 8 | pub const Limits = struct { 9 | identifier_length: usize = 64, 10 | word_length: usize = 64, 11 | path_length: usize = 256, 12 | }; 13 | 14 | fn parseHexDigit(octet: u8) !u4 { 15 | if (!ascii.isHex(octet) or (!ascii.isDigit(octet) and !ascii.isLower(octet))) 16 | return error.InvalidHexLiteral; 17 | 18 | return @truncate(fmt.charToDigit(octet, 16) catch unreachable); 19 | } 20 | 21 | fn parseHexLiteral(comptime T: type, raw: []const u8, fixed_width: bool) !T { 22 | if (fixed_width) { 23 | const w = if (T == u8) 2 else if (T == u16) 4 else unreachable; 24 | 25 | if (raw.len != w) 26 | return error.InvalidHexLiteral; 27 | } 28 | 29 | for (raw) |oct| 30 | if (!ascii.isHex(oct) or (!ascii.isDigit(oct) and !ascii.isLower(oct))) 31 | return error.InvalidHexLiteral; 32 | 33 | return fmt.parseInt(T, raw, 16) catch unreachable; 34 | } 35 | 36 | pub fn Scanner(comptime lim: Limits) type { 37 | return struct { 38 | pub const limits = lim; 39 | 40 | pub const Literal = union(enum) { 41 | byte: u8, 42 | short: u16, 43 | }; 44 | 45 | pub const TypedLabel = union(enum) { 46 | root: Label, 47 | scoped: Label, 48 | }; 49 | 50 | pub const AddressType = enum { 51 | zero, 52 | relative, 53 | absolute, 54 | zero_raw, 55 | relative_raw, 56 | absolute_raw, 57 | }; 58 | 59 | pub const Address = struct { 60 | type: AddressType, 61 | label: TypedLabel, 62 | }; 63 | 64 | pub const Instruction = struct { 65 | mnemonic: []const u8, 66 | encoded: u8, 67 | }; 68 | 69 | pub const Offset = union(enum) { 70 | literal: u16, 71 | label: TypedLabel, 72 | }; 73 | 74 | pub const Padding = union(enum) { 75 | relative: Offset, 76 | absolute: Offset, 77 | }; 78 | 79 | pub const Location = struct { 80 | usize, 81 | usize, 82 | }; 83 | 84 | pub const Label = [limits.identifier_length:0]u8; 85 | 86 | location: Location = .{ 1, 1 }, 87 | 88 | macro_names: std.BoundedArray(Label, 0x100) = 89 | std.BoundedArray(Label, 0x100).init(0) catch unreachable, 90 | 91 | pub const Token = union(enum) { 92 | macro_definition: Label, 93 | curly_close: void, 94 | macro_expansion: Label, 95 | 96 | literal: Literal, 97 | raw_literal: Literal, 98 | label: TypedLabel, 99 | address: Address, 100 | padding: Padding, 101 | instruction: Instruction, 102 | jci: TypedLabel, 103 | jmi: TypedLabel, 104 | jsi: TypedLabel, 105 | word: [limits.word_length:0]u8, 106 | include: [limits.path_length:0]u8, 107 | }; 108 | 109 | pub const SourceToken = struct { 110 | start: Location, 111 | end: Location, 112 | 113 | token: Token, 114 | }; 115 | 116 | pub const Error = error{ 117 | PrematureEof, 118 | InvalidToken, 119 | InvalidHexLiteral, 120 | TokenTooLong, 121 | PathTooLong, 122 | UppercaseLabelForbidden, 123 | }; 124 | 125 | pub fn init() @This() { 126 | return .{}; 127 | } 128 | 129 | fn readByte(scanner: *@This(), input: anytype) ?u8 { 130 | const b = input.readByte() catch return null; 131 | 132 | if (b == '\n') { 133 | scanner.location[0] += 1; 134 | scanner.location[1] = 1; 135 | } else { 136 | scanner.location[1] += 1; 137 | } 138 | 139 | return b; 140 | } 141 | 142 | fn readHexDigit(scanner: *@This(), input: anytype) Error!?u4 { 143 | return try parseHexDigit(scanner.readByte(input) orelse return null); 144 | } 145 | 146 | fn readLiteral(scanner: *@This(), input: anytype) Error!Literal { 147 | const h0n: u8 = try scanner.readHexDigit(input) orelse return error.PrematureEof; 148 | const l0n: u8 = try scanner.readHexDigit(input) orelse return error.PrematureEof; 149 | 150 | // Catch EOF as whitespace so we exit cleanly in case "#xy" is the very last thing in the input 151 | const next = scanner.readByte(input) orelse ' '; 152 | 153 | const h1n: u8 = if (ascii.isWhitespace(next)) 154 | return .{ .byte = @as(u8, h0n << 4) | l0n } 155 | else 156 | try parseHexDigit(next); 157 | 158 | const l1n = try scanner.readHexDigit(input) orelse return error.PrematureEof; 159 | 160 | return .{ 161 | .short = @as(u16, h0n) << 12 | 162 | @as(u16, l0n) << 8 | 163 | @as(u16, h1n) << 4 | 164 | @as(u16, l1n) << 0, 165 | }; 166 | } 167 | 168 | fn readWhitespaceDelimited( 169 | scanner: *@This(), 170 | comptime maxlen: usize, 171 | input: anytype, 172 | ) Error![maxlen:0]u8 { 173 | var output = [1:0]u8{0x00} ** maxlen; 174 | var fbs = std.io.fixedBufferStream(&output); 175 | var writer = fbs.writer(); 176 | 177 | while (true) { 178 | const oct = scanner.readByte(input) orelse ' '; 179 | 180 | if (ascii.isWhitespace(oct)) 181 | break; 182 | 183 | writer.writeByte(oct) catch { 184 | return error.TokenTooLong; 185 | }; 186 | } 187 | 188 | return output; 189 | } 190 | 191 | fn readLabel(scanner: *@This(), input: anytype) Error!Label { 192 | const label = try scanner.readWhitespaceDelimited(limits.identifier_length, input); 193 | 194 | for (label) |oct| { 195 | if (ascii.isLower(oct) or !ascii.isAlphanumeric(oct)) 196 | break; 197 | } else return error.UppercaseLabelForbidden; 198 | 199 | return label; 200 | } 201 | 202 | fn readPath(scanner: *@This(), input: anytype) Error![256:0]u8 { 203 | return scanner.readWhitespaceDelimited(256, input) catch { 204 | return error.PathTooLong; 205 | }; 206 | } 207 | 208 | fn toTypedLabel(label: Label) TypedLabel { 209 | if (label[0] == '&' or label[0] == '/') { 210 | var cpy = label; 211 | 212 | mem.copyForwards(u8, &cpy, cpy[1..]); 213 | 214 | return .{ .scoped = cpy }; 215 | } else { 216 | return .{ .root = label }; 217 | } 218 | } 219 | 220 | fn registerMacro(scanner: *@This(), ident: Label) void { 221 | // TODO 222 | scanner.macro_names.append(ident) catch unreachable; 223 | } 224 | 225 | fn recallMacro(scanner: *@This(), ident: Label) bool { 226 | for (scanner.macro_names.slice()) |n| { 227 | if (mem.eql(u8, &n, &ident)) 228 | return true; 229 | } 230 | 231 | return false; 232 | } 233 | 234 | pub fn readToken(scanner: *@This(), input: anytype) Error!?SourceToken { 235 | var comment_depth: usize = 0; 236 | 237 | while (scanner.readByte(input)) |b| { 238 | if (comment_depth > 0 and (b != ')') and (b != '(')) 239 | continue; 240 | 241 | if (ascii.isWhitespace(b)) 242 | continue; 243 | 244 | var start = scanner.location; 245 | start[1] -= 1; 246 | 247 | errdefer scanner.location = start; 248 | 249 | var end: Location = undefined; 250 | 251 | const token: Token = switch (b) { 252 | '(' => { 253 | comment_depth += 1; 254 | 255 | continue; 256 | }, 257 | ')' => { 258 | comment_depth -= 1; 259 | 260 | continue; 261 | }, 262 | 263 | '[', ']', ' ', '\t', '\n', '\r' => continue, 264 | 265 | '@', '&' => b: { 266 | // Labels 267 | const label = try scanner.readLabel(input); 268 | 269 | end = Location{ start[0], start[1] + 1 + mem.sliceTo(&label, 0).len }; 270 | 271 | break :b if (b == '@') 272 | .{ .label = .{ .root = label } } 273 | else 274 | .{ .label = .{ .scoped = label } }; 275 | }, 276 | 277 | ',', '.', ';', '_', '-', '=', ':' => b: { 278 | // Adressing 279 | const label = try scanner.readLabel(input); 280 | const typed = toTypedLabel(label); 281 | 282 | end = Location{ start[0], start[1] + 1 + mem.sliceTo(&label, 0).len }; 283 | 284 | break :b .{ 285 | .address = .{ 286 | .label = typed, 287 | .type = switch (b) { 288 | '.' => .zero, 289 | '-' => .zero_raw, 290 | ',' => .relative, 291 | '_' => .relative_raw, 292 | ';' => .absolute, 293 | '=', ':' => .absolute_raw, 294 | else => unreachable, 295 | }, 296 | }, 297 | }; 298 | }, 299 | '#' => b: { 300 | // Literal hex 301 | const literal = try scanner.readLiteral(input); 302 | const litlen: usize = switch (literal) { 303 | .byte => 2, 304 | .short => 4, 305 | }; 306 | 307 | end = Location{ start[0], start[1] + 1 + litlen }; 308 | 309 | break :b .{ .literal = literal }; 310 | }, 311 | 312 | '|', '$' => b: { 313 | // Padding 314 | var pad = try scanner.readWhitespaceDelimited(limits.identifier_length, input); 315 | 316 | end = Location{ start[0], start[1] + 1 + mem.sliceTo(&pad, 0).len }; 317 | 318 | const offset: Offset = if (parseHexLiteral(u16, mem.sliceTo(&pad, 0), false) catch null) |lit| 319 | .{ .literal = lit } 320 | else 321 | .{ .label = toTypedLabel(pad) }; 322 | 323 | break :b if (b == '|') 324 | .{ .padding = .{ .absolute = offset } } 325 | else 326 | .{ .padding = .{ .relative = offset } }; 327 | }, 328 | 329 | '?' => b: { 330 | const label = try scanner.readLabel(input); 331 | 332 | end = Location{ start[0], start[1] + 1 + mem.sliceTo(&label, 0).len }; 333 | 334 | break :b .{ .jci = toTypedLabel(label) }; 335 | }, 336 | 337 | '!' => b: { 338 | const label = try scanner.readLabel(input); 339 | 340 | end = Location{ start[0], start[1] + 1 + mem.sliceTo(&label, 0).len }; 341 | 342 | break :b .{ .jmi = toTypedLabel(label) }; 343 | }, 344 | 345 | '~' => b: { 346 | const path = try scanner.readPath(input); 347 | 348 | end = Location{ start[0], start[1] + 1 + mem.sliceTo(&path, 0).len }; 349 | 350 | break :b .{ .include = path }; 351 | }, 352 | 353 | '%' => b: { 354 | const ident = try scanner.readWhitespaceDelimited(limits.identifier_length, input); 355 | 356 | scanner.registerMacro(ident); 357 | 358 | end = Location{ start[0], start[1] + 1 + mem.sliceTo(&ident, 0).len }; 359 | 360 | break :b .{ .macro_definition = ident }; 361 | }, 362 | 363 | '}' => b: { 364 | end = Location{ start[0], start[1] + 1 }; 365 | 366 | break :b .curly_close; 367 | }, 368 | 369 | '"' => b: { 370 | var word = [1:0]u8{0x00} ** 64; 371 | var i: usize = 0; 372 | 373 | while (scanner.readByte(input)) |oct| : (i += 1) { 374 | if (ascii.isWhitespace(oct)) 375 | break; 376 | 377 | word[i] = oct; 378 | } 379 | 380 | end = Location{ start[0], start[1] + 1 + mem.sliceTo(&word, 0).len }; 381 | 382 | break :b .{ .word = word }; 383 | }, 384 | 385 | else => b: { 386 | var needle = [1:0]u8{b} ++ [1:0]u8{0x00} ** (limits.identifier_length - 1); 387 | var remain = try scanner.readWhitespaceDelimited(limits.identifier_length, input); 388 | 389 | end = Location{ start[0], start[1] + 1 + mem.sliceTo(&remain, 0).len }; 390 | 391 | for (1.., mem.sliceTo(&remain, 0)) |j, oct| 392 | needle[j] = oct; 393 | 394 | if (std.meta.stringToEnum(uxn.Cpu.Opcode, std.mem.sliceTo(&needle, 0))) |opcode| { 395 | break :b .{ 396 | .instruction = .{ 397 | .mnemonic = @tagName(opcode), 398 | .encoded = @intFromEnum(opcode), 399 | }, 400 | }; 401 | } else { 402 | const slice = mem.sliceTo(&needle, 0); 403 | 404 | break :b if (parseHexLiteral(u8, slice, true) catch null) |byte| 405 | .{ .raw_literal = .{ .byte = byte } } 406 | else if (parseHexLiteral(u16, slice, true) catch null) |short| 407 | .{ .raw_literal = .{ .short = short } } 408 | else if (scanner.recallMacro(needle)) 409 | .{ .macro_expansion = needle } 410 | else 411 | .{ .jsi = toTypedLabel(needle) }; 412 | } 413 | }, 414 | }; 415 | 416 | return .{ 417 | .start = start, 418 | .end = end, 419 | .token = token, 420 | }; 421 | } 422 | 423 | return null; 424 | } 425 | }; 426 | } 427 | -------------------------------------------------------------------------------- /src/lib/uxn/Cpu.zig: -------------------------------------------------------------------------------- 1 | const Cpu = @This(); 2 | 3 | pub const Stack = @import("cpu/Stack.zig"); 4 | 5 | const std = @import("std"); 6 | const logger = std.log.scoped(.uxn_cpu); 7 | 8 | pub const faults_enabled = @import("lib.zig").faults_enabled; 9 | pub const page_size = 0x10000; 10 | pub const device_page_size = 0x100; 11 | 12 | const isa = @import("cpu/isa.zig"); 13 | 14 | pub const Opcode = isa.Opcode; 15 | 16 | pub const SystemFault = error{ 17 | StackOverflow, 18 | StackUnderflow, 19 | 20 | DivisionByZero, 21 | 22 | BadExpansion, 23 | }; 24 | 25 | pub fn isCatchable(f: SystemFault) bool { 26 | return f != error.BadExpansion; 27 | } 28 | 29 | pub const InterceptKind = enum { 30 | input, 31 | output, 32 | }; 33 | 34 | const StackSet = struct { 35 | primary: *Stack, 36 | secondary: *Stack, 37 | }; 38 | 39 | pc: u16, 40 | 41 | wst: Stack, 42 | rst: Stack, 43 | 44 | // Saved stack pointers 45 | wst_sp: ?u8 = null, 46 | rst_sp: ?u8 = null, 47 | 48 | // Pointers to active stacks 49 | primary_stack: *Stack = undefined, 50 | secondary_stack: *Stack = undefined, 51 | 52 | mem: *[page_size]u8, 53 | device_mem: [device_page_size]u8, 54 | 55 | input_intercepts: [0x10]u16 = [1]u16{0x0000} ** 0x10, 56 | output_intercepts: [0x10]u16 = [1]u16{0x0000} ** 0x10, 57 | 58 | callback_data: ?*anyopaque = null, 59 | 60 | device_intercept: ?*const fn ( 61 | cpu: *Cpu, 62 | addr: u8, 63 | kind: InterceptKind, 64 | data: ?*anyopaque, 65 | ) SystemFault!void = null, 66 | 67 | pub fn init(memory: *[page_size]u8) Cpu { 68 | var cpu = Cpu{ 69 | .pc = 0x0100, 70 | 71 | .wst = Stack.init(), 72 | .rst = Stack.init(), 73 | 74 | .mem = memory, 75 | .device_mem = [1]u8{0x00} ** 0x100, 76 | }; 77 | 78 | if (faults_enabled) { 79 | cpu.wst.xflow_behaviour = .fault; 80 | cpu.rst.xflow_behaviour = .fault; 81 | } 82 | 83 | return cpu; 84 | } 85 | 86 | pub fn evaluateVector(cpu: *Cpu, vector: u16) SystemFault!void { 87 | cpu.pc = vector; 88 | 89 | logger.debug("Vector {x:0>4}: Start evaluation", .{vector}); 90 | 91 | errdefer |err| { 92 | logger.debug("Vector {x:0>4}: Faulted with {}", .{ vector, err }); 93 | } 94 | 95 | if (try cpu.run(null)) |_| { 96 | logger.debug("Ran to completion!", .{}); 97 | } 98 | 99 | logger.debug("Vector {x:0>4}: Finished evaluation", .{vector}); 100 | } 101 | 102 | inline fn load( 103 | cpu: *const Cpu, 104 | comptime T: type, 105 | comptime field: []const u8, 106 | addr: anytype, 107 | comptime boundary: usize, 108 | ) T { 109 | return if (T == u8) 110 | @field(cpu, field)[addr] 111 | else switch (@typeInfo(T)) { 112 | .@"struct" => |s| if (s.backing_integer) |U| 113 | @bitCast(cpu.load(U, field, addr, boundary)) 114 | else 115 | @panic("Cannot read arbitrary struct types"), 116 | 117 | .int => if (@as(usize, addr) + @sizeOf(T) <= boundary) 118 | std.mem.readInt(T, @as(*const [@sizeOf(T)]u8, @ptrCast(@field(cpu, field)[addr..addr +| @sizeOf(T)])), .big) 119 | else r: { 120 | var b: T = undefined; 121 | 122 | inline for (0..@sizeOf(T)) |i| { 123 | b <<= 8; 124 | b |= cpu.load(u8, field, (addr +% i) % boundary, boundary); 125 | } 126 | 127 | break :r b; 128 | }, 129 | 130 | else => @panic("Can only read bitfield structures and integers"), 131 | }; 132 | } 133 | 134 | inline fn store( 135 | cpu: *Cpu, 136 | comptime T: type, 137 | comptime field: []const u8, 138 | addr: anytype, 139 | val: T, 140 | comptime boundary: usize, 141 | ) void { 142 | if (T == u8) 143 | @field(cpu, field)[addr] = val 144 | else switch (@typeInfo(T)) { 145 | .@"struct" => |s| if (s.backing_integer) |U| 146 | cpu.store(U, field, addr, val, boundary) 147 | else 148 | @panic("Cannot store arbitrary struct types"), 149 | 150 | .int => if (@as(usize, addr) + @sizeOf(T) <= boundary) { 151 | std.mem.writeInt( 152 | T, 153 | @as(*[@sizeOf(T)]u8, @ptrCast(@field(cpu, field)[addr..addr +| @sizeOf(T)])), 154 | val, 155 | .big, 156 | ); 157 | } else { 158 | inline for (0.., std.mem.asBytes(&std.mem.nativeToBig(T, val))) |i, oct| { 159 | cpu.store(u8, field, (addr + i) % boundary, oct, boundary); 160 | } 161 | }, 162 | 163 | else => @panic("Can only store bitfield structures and integers"), 164 | } 165 | } 166 | 167 | pub fn loadZero(cpu: *const Cpu, comptime T: type, addr: u8) T { 168 | return cpu.load(T, "mem", addr, 0x100); 169 | } 170 | 171 | pub fn loadMem(cpu: *const Cpu, comptime T: type, addr: u16) T { 172 | return cpu.load(T, "mem", addr, 0x10000); 173 | } 174 | 175 | pub fn loadDeviceMem(cpu: *const Cpu, comptime T: type, addr: u8) T { 176 | return cpu.load(T, "device_mem", addr, 0x100); 177 | } 178 | 179 | pub fn storeZero(cpu: *Cpu, comptime T: type, addr: u8, v: T) void { 180 | cpu.store(T, "mem", addr, v, 0x100); 181 | } 182 | 183 | pub fn storeMem(cpu: *Cpu, comptime T: type, addr: u16, v: T) void { 184 | cpu.store(T, "mem", addr, v, 0x10000); 185 | } 186 | 187 | pub fn storeDeviceMem(cpu: *Cpu, comptime T: type, addr: u8, v: T) void { 188 | cpu.store(T, "device_mem", addr, v, 0x100); 189 | } 190 | 191 | fn addRelative(addr: u16, offset: u8) u16 { 192 | return @bitCast(@as(i16, @bitCast(addr)) +% @as(i8, @bitCast(offset))); 193 | } 194 | 195 | /// Prepare the CPU for executing the given opcode. This includes saving the 196 | /// stack pointers in case of `k`-opcodes, and swapping the stack pointers 197 | /// for `r`-opcodes. 198 | fn prepareExecute(cpu: *Cpu, comptime opcode: Opcode) void { 199 | if (opcode.returnMode()) { 200 | cpu.primary_stack = &cpu.rst; 201 | cpu.secondary_stack = &cpu.wst; 202 | } else { 203 | cpu.primary_stack = &cpu.wst; 204 | cpu.secondary_stack = &cpu.rst; 205 | } 206 | 207 | if (opcode.keepMode() and opcode.baseOpcode() != .BRK) { 208 | cpu.wst_sp = cpu.wst.sp; 209 | cpu.rst_sp = cpu.rst.sp; 210 | } 211 | } 212 | 213 | /// Finish the pre-execution path of the currently executing opcode. If the stacks 214 | /// were frozen for `k`-opcodes, this resets them back to their saved locations. 215 | fn finishPreExecute(cpu: *Cpu) void { 216 | if (cpu.wst_sp) |sp| { 217 | cpu.wst.sp = sp; 218 | cpu.wst_sp = null; 219 | } 220 | 221 | if (cpu.rst_sp) |sp| { 222 | cpu.rst.sp = sp; 223 | cpu.rst_sp = null; 224 | } 225 | } 226 | 227 | inline fn fetchJump(cpu: *Cpu, pc: u16) Opcode { 228 | defer cpu.pc = pc; 229 | 230 | return .fromByte(cpu.mem[cpu.pc]); 231 | } 232 | 233 | inline fn fetchNext(cpu: *Cpu) Opcode { 234 | switch (Opcode.fromByte(cpu.mem[cpu.pc])) { 235 | inline else => |opcode| cpu.prepareExecute(opcode), 236 | } 237 | 238 | return cpu.fetchJump(cpu.pc +% 1); 239 | } 240 | 241 | inline fn fetchImmedate(cpu: *Cpu, comptime T: type) T { 242 | defer cpu.pc +%= @sizeOf(T); 243 | 244 | return cpu.loadMem(T, cpu.pc); 245 | } 246 | 247 | /// Execute a memory -> stack push. 248 | inline fn executeLiteralPush(cpu: *Cpu, comptime T: type) !void { 249 | try cpu.primary_stack.push(T, cpu.fetchImmedate(T)); 250 | } 251 | 252 | /// Execute a jump based on a 16 bit relative immediate offset. Depending on 253 | /// `push_ret` and `conditional`, this is either a straight jump, a subroutine 254 | /// call or stack-conditional jump. 255 | inline fn executeImmediateJump( 256 | cpu: *Cpu, 257 | comptime push_ret: bool, 258 | comptime conditional: bool, 259 | ) !void { 260 | const offset = cpu.fetchImmedate(u16); 261 | 262 | if (push_ret) { 263 | try cpu.rst.push(u16, cpu.pc); 264 | } 265 | 266 | _ = cpu.fetchJump(if (!conditional or try cpu.wst.pop(u8) > 0x00) 267 | cpu.pc +% offset 268 | else 269 | cpu.pc); 270 | } 271 | 272 | /// Execute a jump based on an 8 or 16 bit relative_raw offset on the stack. Depending on 273 | /// `push_ret` and `conditional`, this is either a straight jump, a subroutine 274 | /// call or stack-conditional jump. 275 | inline fn executeStackJump( 276 | cpu: *Cpu, 277 | comptime T: type, 278 | comptime push_ret: bool, 279 | comptime conditional: bool, 280 | ) !void { 281 | const operand = try cpu.primary_stack.pop(T); 282 | 283 | const do_jump = if (conditional) 284 | try cpu.primary_stack.pop(u8) > 0x00 285 | else 286 | true; 287 | 288 | cpu.finishPreExecute(); 289 | 290 | if (push_ret) { 291 | try cpu.secondary_stack.push(u16, cpu.pc); 292 | } 293 | 294 | _ = cpu.fetchJump(if (do_jump and T == u16) 295 | operand 296 | else if (do_jump and T == u8) 297 | addRelative(cpu.pc, operand) 298 | else 299 | cpu.pc); 300 | } 301 | 302 | /// Execute a "stack shuffle" operation that consists of popping `N` elements 303 | /// and re-pushing them or some in the specified order. 304 | inline fn executeStackShuffle( 305 | cpu: *Cpu, 306 | comptime T: type, 307 | comptime N: usize, 308 | out_order: anytype, 309 | ) !void { 310 | var popped: [N]T = undefined; 311 | 312 | inline for (&popped) |*p| 313 | p.* = try cpu.primary_stack.pop(T); 314 | 315 | cpu.finishPreExecute(); 316 | 317 | inline for (out_order) |i| 318 | try cpu.primary_stack.push(T, popped[i]); 319 | } 320 | 321 | pub fn run(cpu: *Cpu, step_limit: ?usize) SystemFault!?u16 { 322 | @setEvalBranchQuota(2048); 323 | 324 | var step: usize = 0; 325 | 326 | return while (Opcode.fromByte(cpu.mem[cpu.pc]) != .BRK) { 327 | if (step_limit) |limit| { 328 | if (step >= limit) { 329 | return null; 330 | } 331 | 332 | step += 1; 333 | } 334 | 335 | logger.debug("PC {x:0>4}: Start execute {s}", .{ 336 | cpu.pc, 337 | @tagName(@as(Opcode, @enumFromInt(cpu.mem[cpu.pc]))), 338 | }); 339 | 340 | errdefer |err| { 341 | logger.debug("PC {x:0>4}: {s}: Faulting with {}", .{ 342 | cpu.pc, 343 | @tagName(@as(Opcode, @enumFromInt(cpu.mem[cpu.pc]))), 344 | err, 345 | }); 346 | } 347 | 348 | switch (cpu.fetchNext()) { 349 | // Special case literals and immediates since they overload the BRK 350 | // base opcode. 351 | inline .LIT, .LIT2, .LITr, .LIT2r => |opcode| { 352 | try cpu.executeLiteralPush(opcode.nativeOperandType()); 353 | }, 354 | 355 | inline .JCI, .JMI, .JSI => |opcode| { 356 | try cpu.executeImmediateJump( 357 | opcode == .JSI, 358 | opcode == .JCI, 359 | ); 360 | }, 361 | 362 | // For everything else, piggyback off the base opcode. 363 | inline else => |opcode| { 364 | const T = opcode.nativeOperandType(); 365 | 366 | switch (opcode.baseOpcode()) { 367 | .BRK => unreachable, 368 | 369 | .JMP, .JSR, .JCN => { 370 | try cpu.executeStackJump( 371 | T, 372 | opcode.baseOpcode() == .JSR, 373 | opcode.baseOpcode() == .JCN, 374 | ); 375 | }, 376 | 377 | .POP, .NIP, .SWP, .ROT, .DUP, .OVR => { 378 | const n = switch (opcode.baseOpcode()) { 379 | .POP, .DUP => 1, 380 | .NIP, .SWP, .OVR => 2, 381 | .ROT => 3, 382 | else => unreachable, 383 | }; 384 | 385 | const order = switch (opcode.baseOpcode()) { 386 | .POP => .{}, 387 | .NIP => .{0}, 388 | .SWP => .{ 0, 1 }, 389 | .ROT => .{ 1, 0, 2 }, 390 | .DUP => .{ 0, 0 }, 391 | .OVR => .{ 1, 0, 1 }, 392 | else => unreachable, 393 | }; 394 | 395 | try cpu.executeStackShuffle(T, n, order); 396 | }, 397 | 398 | .STH => { 399 | const val = try cpu.primary_stack.pop(T); 400 | 401 | cpu.finishPreExecute(); 402 | 403 | try cpu.secondary_stack.push(T, val); 404 | }, 405 | 406 | .EQU, .NEQ, .GTH, .LTH => { 407 | const b = try cpu.primary_stack.pop(T); 408 | const a = try cpu.primary_stack.pop(T); 409 | 410 | cpu.finishPreExecute(); 411 | 412 | try cpu.primary_stack.push(u8, @intFromBool(switch (opcode.baseOpcode()) { 413 | .EQU => a == b, 414 | .NEQ => a != b, 415 | .GTH => a > b, 416 | .LTH => a < b, 417 | 418 | else => unreachable, 419 | })); 420 | }, 421 | 422 | .ADD, .SUB, .MUL, .DIV, .AND, .ORA, .EOR => { 423 | const b = try cpu.primary_stack.pop(T); 424 | const a = try cpu.primary_stack.pop(T); 425 | 426 | cpu.finishPreExecute(); 427 | 428 | try cpu.primary_stack.push(T, switch (opcode.baseOpcode()) { 429 | .ADD => a +% b, 430 | .SUB => a -% b, 431 | .MUL => a *% b, 432 | .DIV => if (b != 0) 433 | a / b 434 | else if (!faults_enabled) 435 | 0 436 | else 437 | return error.DivisionByZero, 438 | 439 | .AND => a & b, 440 | .ORA => a | b, 441 | .EOR => a ^ b, 442 | 443 | else => unreachable, 444 | }); 445 | }, 446 | 447 | .INC => { 448 | const val = try cpu.primary_stack.pop(T); 449 | 450 | cpu.finishPreExecute(); 451 | 452 | try cpu.primary_stack.push(T, val +% 1); 453 | }, 454 | 455 | .SFT => { 456 | const shift = try cpu.primary_stack.pop(u8); 457 | const operand = try cpu.primary_stack.pop(T); 458 | 459 | cpu.finishPreExecute(); 460 | 461 | const rshift: u4 = @truncate(shift & 0xf); 462 | const lshift: u4 = @truncate(shift >> 4); 463 | 464 | // If the operand would be shifted beyond its bit size, break :r 0 465 | try cpu.primary_stack.push( 466 | T, 467 | if (rshift < @bitSizeOf(T) and lshift < @bitSizeOf(T)) 468 | operand >> @truncate(rshift) << @truncate(lshift) 469 | else 470 | 0, 471 | ); 472 | }, 473 | 474 | .DEI, .LDZ, .LDR, .LDA, .DEO, .STZ, .STR, .STA => |op| { 475 | const st = cpu.primary_stack; 476 | 477 | const addr = switch (op) { 478 | inline .LDA, .STA => try st.pop(u16), 479 | inline .DEI, .DEO, .LDZ, .STZ => try st.pop(u8), 480 | else => addRelative(cpu.pc, try st.pop(u8)), 481 | }; 482 | 483 | switch (op) { 484 | inline .DEI, .LDZ, .LDR, .LDA => { 485 | cpu.finishPreExecute(); 486 | 487 | if (op == .DEI) { 488 | const dev: u8 = @truncate(addr); 489 | 490 | const intercept_mask = cpu.input_intercepts[dev >> 4]; 491 | const intercept_port = intercept_mask >> @truncate(dev & 0xf); 492 | 493 | if (intercept_port & 0x1 > 0) 494 | if (cpu.device_intercept) |ifn| 495 | try ifn( 496 | cpu, 497 | dev, 498 | .input, 499 | cpu.callback_data, 500 | ); 501 | 502 | if (opcode.shortMode() and (intercept_port >> 1) & 0x1 > 0) 503 | if (cpu.device_intercept) |ifn| 504 | try ifn( 505 | cpu, 506 | dev + 1, 507 | .input, 508 | cpu.callback_data, 509 | ); 510 | } 511 | 512 | try st.push(T, switch (op) { 513 | inline .DEI => cpu.loadDeviceMem(T, @truncate(addr)), 514 | inline .LDZ => cpu.loadZero(T, @truncate(addr)), 515 | inline .LDR, .LDA => cpu.loadMem(T, @truncate(addr)), 516 | 517 | else => unreachable, 518 | }); 519 | }, 520 | 521 | inline .DEO, .STZ, .STR, .STA => { 522 | const value = try st.pop(T); 523 | 524 | cpu.finishPreExecute(); 525 | 526 | switch (op) { 527 | inline .DEO => cpu.storeDeviceMem(T, addr, value), 528 | inline .STZ => cpu.storeZero(T, addr, value), 529 | inline .STR, .STA => cpu.storeMem(T, addr, value), 530 | 531 | else => unreachable, 532 | } 533 | 534 | if (op == .DEO) { 535 | const dev: u8 = @truncate(addr); 536 | 537 | const intercept_mask = cpu.output_intercepts[dev >> 4]; 538 | const intercept_port = intercept_mask >> @truncate(dev & 0xf); 539 | 540 | if (intercept_port & 0x1 > 0) 541 | if (cpu.device_intercept) |ifn| 542 | try ifn( 543 | cpu, 544 | dev, 545 | .output, 546 | cpu.callback_data, 547 | ); 548 | 549 | if (opcode.shortMode() and (intercept_port >> 1) & 0x1 > 0) 550 | if (cpu.device_intercept) |ifn| 551 | try ifn( 552 | cpu, 553 | dev + 1, 554 | .output, 555 | cpu.callback_data, 556 | ); 557 | } 558 | }, 559 | 560 | else => unreachable, 561 | } 562 | }, 563 | } 564 | }, 565 | } 566 | } else cpu.pc; 567 | } 568 | -------------------------------------------------------------------------------- /src/lib/uxn/cpu/Stack.zig: -------------------------------------------------------------------------------- 1 | const Stack = @This(); 2 | 3 | const std = @import("std"); 4 | 5 | data: [0x100]u8, 6 | sp: u8, 7 | 8 | xflow_behaviour: enum { 9 | wrap, 10 | fault, 11 | } = .wrap, 12 | 13 | pub fn init() Stack { 14 | return .{ 15 | .data = [1]u8{0x00} ** 0x100, 16 | .sp = 0, 17 | }; 18 | } 19 | 20 | inline fn pushByte(s: *Stack, byte: u8) !void { 21 | if (s.xflow_behaviour == .fault and s.sp > 0xfe) { 22 | return error.StackOverflow; 23 | } 24 | 25 | s.data[s.sp] = byte; 26 | s.sp +%= 1; 27 | } 28 | 29 | inline fn popByte(s: *Stack) !u8 { 30 | if (s.xflow_behaviour == .fault and s.sp == 0) { 31 | return error.StackUnderflow; 32 | } 33 | 34 | defer { 35 | s.sp -%= 1; 36 | } 37 | 38 | return s.data[s.sp -% 1]; 39 | } 40 | 41 | pub fn push(s: *Stack, comptime T: type, v: T) !void { 42 | inline for (0..@sizeOf(T)) |i| { 43 | try s.pushByte(@truncate(v >> @truncate((@sizeOf(T) - 1 - i) * 8))); 44 | } 45 | } 46 | 47 | pub fn pop(s: *Stack, comptime T: type) !T { 48 | var res: T = 0; 49 | 50 | inline for (0..@sizeOf(T)) |i| { 51 | res |= @as(T, try s.popByte()) << @truncate(i * 8); 52 | } 53 | 54 | return res; 55 | } 56 | -------------------------------------------------------------------------------- /src/lib/uxn/cpu/isa.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const config = @import("config"); 3 | 4 | /// Describes an operands of an instruction. 5 | pub const Operand = struct { 6 | /// Describes the size of an operand, in bytes, on the stack. 7 | pub const StackSize = enum { 8 | /// Size is 8 bits if the instruction is in byte mode, otherwise 16 bits. 9 | auto, 10 | 11 | /// Size is guaranteed to be 8 bits. 12 | byte, 13 | 14 | /// Size is guaranteed to be 16 bits. 15 | short, 16 | }; 17 | 18 | /// The name of the operand. 19 | name: []const u8, 20 | 21 | /// The size of the operand. 22 | size: StackSize, 23 | 24 | /// Create a new named operand with the given stack size. 25 | fn sized(comptime name: []const u8, size: StackSize) Operand { 26 | return Operand{ 27 | .name = name, 28 | .size = size, 29 | }; 30 | } 31 | 32 | fn replaceAuto(operand: Operand, size: StackSize) Operand { 33 | return Operand{ 34 | .name = operand.name, 35 | .size = if (operand.size == .auto) size else operand.size, 36 | }; 37 | } 38 | }; 39 | 40 | /// The "base" opcode of an instruction, before the various modifiers are 41 | /// applied. (which may change meaning significantly in the case of `BRK`) 42 | pub const BaseOpcode = enum(u5) { 43 | /// `( -- a )` (LIT) 44 | /// `( cond8 -- )` (JCI) 45 | /// `( -- )` (BRK, JSI, JMI) 46 | BRK, 47 | 48 | /// `( a -- a+1 ) 49 | INC, 50 | 51 | /// `( a -- ) 52 | POP, 53 | 54 | /// `( a b -- b ) 55 | NIP, 56 | 57 | /// `( a b -- b a ) 58 | SWP, 59 | 60 | /// `( a b c -- b c a ) 61 | ROT, 62 | 63 | /// `( a -- a a ) 64 | DUP, 65 | 66 | /// `( a b -- a b a ) 67 | OVR, 68 | 69 | /// `( a b -- bool8 ) 70 | EQU, 71 | 72 | /// `( a b -- bool8 ) 73 | NEQ, 74 | 75 | /// `( a b -- bool8 ) 76 | GTH, 77 | 78 | /// `( a b -- bool8 ) 79 | LTH, 80 | 81 | /// `( addr -- ) 82 | JMP, 83 | 84 | /// `( cond8 addr -- ) 85 | JCN, 86 | 87 | /// `( addr -- | ret16 ) 88 | JSR, 89 | 90 | /// `( a -- | a ) 91 | STH, 92 | 93 | /// `( addr8 -- v )` 94 | LDZ, 95 | 96 | /// `( v addr8 -- )` 97 | STZ, 98 | 99 | /// `( addr8 -- v )` 100 | LDR, 101 | 102 | /// `( v addr8 -- )` 103 | STR, 104 | 105 | /// `( addr16 -- v )` 106 | LDA, 107 | 108 | /// `( v addr16 -- )` 109 | STA, 110 | 111 | /// `( device8 -- v )` 112 | DEI, 113 | 114 | /// `( v device8 -- )` 115 | DEO, 116 | 117 | /// `( a b -- a+b )` 118 | ADD, 119 | 120 | /// `( a b -- a-b )` 121 | SUB, 122 | 123 | /// `( a b -- a*b )` 124 | MUL, 125 | 126 | /// `( a b -- a/b )` 127 | DIV, 128 | 129 | /// `( a b -- a&b )` 130 | AND, 131 | 132 | /// `( a b -- a|b )` 133 | ORA, 134 | 135 | /// `( a b -- a^b )` 136 | EOR, 137 | 138 | /// `( a shift8 -- c )` 139 | SFT, 140 | }; 141 | 142 | /// This entire table *could* be generated by enumerating all possible u8s in 143 | /// comptime and branching off the top 3 bits, but comptime generated enums 144 | /// and structs cannot contain declarations, which are nice to have, plus this 145 | /// doubles as a convenient u8 -> mnemonic lookup table. 146 | pub const Opcode = enum(u8) { 147 | // Please don't mess up my table. 148 | // zig fmt: off 149 | BRK, INC, POP, NIP, SWP, ROT, DUP, OVR, EQU, NEQ, GTH, LTH, JMP, JCN, JSR, STH, 150 | LDZ, STZ, LDR, STR, LDA, STA, DEI, DEO, ADD, SUB, MUL, DIV, AND, ORA, EOR, SFT, 151 | JCI, INC2, POP2, NIP2, SWP2, ROT2, DUP2, OVR2, EQU2, NEQ2, GTH2, LTH2, JMP2, JCN2, JSR2, STH2, 152 | LDZ2, STZ2, LDR2, STR2, LDA2, STA2, DEI2, DEO2, ADD2, SUB2, MUL2, DIV2, AND2, ORA2, EOR2, SFT2, 153 | JMI, INCr, POPr, NIPr, SWPr, ROTr, DUPr, OVRr, EQUr, NEQr, GTHr, LTHr, JMPr, JCNr, JSRr, STHr, 154 | LDZr, STZr, LDRr, STRr, LDAr, STAr, DEIr, DEOr, ADDr, SUBr, MULr, DIVr, ANDr, ORAr, EORr, SFTr, 155 | JSI, INC2r, POP2r, NIP2r, SWP2r, ROT2r, DUP2r, OVR2r, EQU2r, NEQ2r, GTH2r, LTH2r, JMP2r, JCN2r, JSR2r, STH2r, 156 | LDZ2r, STZ2r, LDR2r, STR2r, LDA2r, STA2r, DEI2r, DEO2r, ADD2r, SUB2r, MUL2r, DIV2r, AND2r, ORA2r, EOR2r, SFT2r, 157 | LIT, INCk, POPk, NIPk, SWPk, ROTk, DUPk, OVRk, EQUk, NEQk, GTHk, LTHk, JMPk, JCNk, JSRk, STHk, 158 | LDZk, STZk, LDRk, STRk, LDAk, STAk, DEIk, DEOk, ADDk, SUBk, MULk, DIVk, ANDk, ORAk, EORk, SFTk, 159 | LIT2, INC2k, POP2k, NIP2k, SWP2k, ROT2k, DUP2k, OVR2k, EQU2k, NEQ2k, GTH2k, LTH2k, JMP2k, JCN2k, JSR2k, STH2k, 160 | LDZ2k, STZ2k, LDR2k, STR2k, LDA2k, STA2k, DEI2k, DEO2k, ADD2k, SUB2k, MUL2k, DIV2k, AND2k, ORA2k, EOR2k, SFT2k, 161 | LITr, INCkr, POPkr, NIPkr, SWPkr, ROTkr, DUPkr, OVRkr, EQUkr, NEQkr, GTHkr, LTHkr, JMPkr, JCNkr, JSRkr, STHkr, 162 | LDZkr, STZkr, LDRkr, STRkr, LDAkr, STAkr, DEIkr, DEOkr, ADDkr, SUBkr, MULkr, DIVkr, ANDkr, ORAkr, EORkr, SFTkr, 163 | LIT2r, INC2kr, POP2kr, NIP2kr, SWP2kr, ROT2kr, DUP2kr, OVR2kr, EQU2kr, NEQ2kr, GTH2kr, LTH2kr, JMP2kr, JCN2kr, JSR2kr, STH2kr, 164 | LDZ2kr, STZ2kr, LDR2kr, STR2kr, LDA2kr, STA2kr, DEI2kr, DEO2kr, ADD2kr, SUB2kr, MUL2kr, DIV2kr, AND2kr, ORA2kr, EOR2kr, SFT2kr, 165 | // zig fmt: on 166 | 167 | pub inline fn fromByte(raw: u8) Opcode { 168 | return @enumFromInt(raw); 169 | } 170 | 171 | pub inline fn asByte(opcode: Opcode) u8 { 172 | return @intFromEnum(opcode); 173 | } 174 | 175 | pub fn mnemonic(opcode: Opcode) []const u8 { 176 | return @tagName(opcode); 177 | } 178 | 179 | pub inline fn baseOpcode(opcode: Opcode) BaseOpcode { 180 | return @enumFromInt(@intFromEnum(opcode) & 0x1f); 181 | } 182 | 183 | pub inline fn shortMode(opcode: Opcode) bool { 184 | return (@intFromEnum(opcode) & 0x20) > 0; 185 | } 186 | 187 | pub inline fn nativeOperandType(opcode: Opcode) type { 188 | return if (opcode.shortMode()) u16 else u8; 189 | } 190 | 191 | pub inline fn returnMode(opcode: Opcode) bool { 192 | return (@intFromEnum(opcode) & 0x40) > 0; 193 | } 194 | 195 | pub inline fn keepMode(opcode: Opcode) bool { 196 | return (@intFromEnum(opcode) & 0x80) > 0; 197 | } 198 | }; 199 | 200 | fn readStackOperands(comptime notation: []const u8) []const Operand { 201 | var tokenizer = std.mem.tokenizeScalar( 202 | u8, 203 | notation, 204 | ' ', 205 | ); 206 | 207 | var result: []const Operand = &.{}; 208 | 209 | while (tokenizer.next()) |operand| { 210 | result = result ++ [1]Operand{Operand.sized( 211 | operand, 212 | if (std.mem.endsWith(u8, operand, "8")) 213 | .byte 214 | else if (std.mem.endsWith(u8, operand, "16")) 215 | .short 216 | else 217 | .auto, 218 | )}; 219 | } 220 | 221 | return result; 222 | } 223 | 224 | fn readEffectNotationSide( 225 | comptime notation: []const u8, 226 | ) struct { []const Operand, []const Operand } { 227 | var stacks_iter = std.mem.splitScalar( 228 | u8, 229 | notation, 230 | '|', 231 | ); 232 | 233 | const wst_notation = stacks_iter.next() orelse unreachable; 234 | 235 | if (stacks_iter.next()) |rst_notation| { 236 | return .{ 237 | readStackOperands(wst_notation), 238 | readStackOperands(rst_notation), 239 | }; 240 | } else { 241 | return .{ readStackOperands(wst_notation), &.{} }; 242 | } 243 | } 244 | 245 | /// Holds information about the operands and result stack makeup for a specific 246 | /// stack. (working or return stack) 247 | pub const StackEffect = struct { 248 | /// The operands expected to reside on the given stack. 249 | before: []const Operand = &.{}, 250 | 251 | /// The stack makeup after the effect has been applied. If the instruction 252 | /// was in keep-mode, this will not include the implicit before state that 253 | /// will remain on the stack. 254 | after: []const Operand = &.{}, 255 | 256 | fn replaceAuto(comptime eff: StackEffect, size: Operand.StackSize) StackEffect { 257 | var new_before: []const Operand = &.{}; 258 | var new_after: []const Operand = &.{}; 259 | 260 | for (eff.before) |old_before| { 261 | new_before = new_before ++ [1]Operand{old_before.replaceAuto(size)}; 262 | } 263 | 264 | for (eff.after) |old_after| { 265 | new_after = new_after ++ [1]Operand{old_after.replaceAuto(size)}; 266 | } 267 | 268 | return StackEffect{ 269 | .before = new_before, 270 | .after = new_after, 271 | }; 272 | } 273 | }; 274 | 275 | /// A structure describing the expected stack structures and effects of an 276 | /// instruction. If the instruction is in keep-mode, the `before` states of 277 | /// the stacks are implicitely prefixed to the `after` states and not duplicated 278 | /// within them. 279 | pub const StackEffects = struct { 280 | /// Effects applied to the working stack. (Adjusted for return-mode) 281 | working_stack: StackEffect = .{}, 282 | 283 | /// Effects applied to the working stack. (Adjusted for return-mode) 284 | return_stack: StackEffect = .{}, 285 | 286 | /// Parse the "standard" stack effect notation (e.g. `"a b | ra rb -- b | rb"`) 287 | /// into a structure of operands that can be visualized in a debugger. 288 | fn fromEffectNotation(comptime notation: []const u8) StackEffects { 289 | var iter = std.mem.splitSequence( 290 | u8, 291 | notation, 292 | "--", 293 | ); 294 | 295 | const before = iter.next() orelse unreachable; 296 | const after = iter.next() orelse unreachable; 297 | 298 | const wst_in, const rst_in = readEffectNotationSide(before); 299 | const wst_out, const rst_out = readEffectNotationSide(after); 300 | 301 | return StackEffects{ 302 | .working_stack = StackEffect{ 303 | .before = wst_in, 304 | .after = wst_out, 305 | }, 306 | .return_stack = StackEffect{ 307 | .before = rst_in, 308 | .after = rst_out, 309 | }, 310 | }; 311 | } 312 | 313 | /// Returns a new structure where the input effects are prepended to the 314 | /// output effects, mirroring what the keep-mode does for instructions. 315 | fn keepInputs(eff: StackEffects) StackEffects { 316 | return StackEffects{ 317 | .working_stack = StackEffect{ 318 | .before = eff.working_stack.before, 319 | .after = eff.working_stack.before ++ eff.working_stack.after, 320 | }, 321 | 322 | .return_stack = StackEffect{ 323 | .before = eff.return_stack.before, 324 | .after = eff.return_stack.before ++ eff.return_stack.after, 325 | }, 326 | }; 327 | } 328 | 329 | /// Returns a new structure where the auto size operands are fixed size. 330 | fn replaceAuto(eff: StackEffects, size: Operand.StackSize) StackEffects { 331 | return StackEffects{ 332 | .working_stack = eff.working_stack.replaceAuto(size), 333 | .return_stack = eff.return_stack.replaceAuto(size), 334 | }; 335 | } 336 | }; 337 | 338 | fn generateEffects() [0x100]StackEffects { 339 | // This is comparatively expensive, but only run during compilation of course. 340 | @setEvalBranchQuota(8192); 341 | 342 | var effects_r = [1]StackEffects{undefined} ** 0x100; 343 | 344 | // Skip BRK 345 | var raw_instruction: u8 = 0x00; 346 | 347 | while (raw_instruction < 0x20) : (raw_instruction += 1) { 348 | const instruction = Opcode.fromByte(raw_instruction); 349 | 350 | if (instruction.baseOpcode() == .BRK) { 351 | // The BRK instruction is overloaded with 3 entirely different effects 352 | // depending on its flags that completely change its meaning, so 353 | // it gets specialized here. 354 | 355 | // BRK 356 | effects_r[0x00] = StackEffects.fromEffectNotation("--"); 357 | 358 | // JCI, JMI, JSI 359 | const jxi_eff = StackEffects.fromEffectNotation("addr8 --"); 360 | 361 | effects_r[0x20] = jxi_eff; 362 | effects_r[0x40] = jxi_eff; 363 | effects_r[0x60] = jxi_eff; 364 | 365 | // LIT, LIT2, LITr, LIT2r 366 | const lit_eff = StackEffects.fromEffectNotation("-- a"); 367 | 368 | effects_r[0x80] = lit_eff; 369 | effects_r[0xa0] = lit_eff; 370 | effects_r[0xc0] = lit_eff; 371 | effects_r[0xe0] = lit_eff; 372 | } else { 373 | // Determine the default stack effect of the instruction before 374 | // modifiers are applied. 375 | const default = switch (instruction.baseOpcode()) { 376 | .BRK => unreachable, 377 | 378 | .ADD => StackEffects.fromEffectNotation("a b -- a+b"), 379 | .SUB => StackEffects.fromEffectNotation("a b -- a-b"), 380 | .MUL => StackEffects.fromEffectNotation("a b -- a*b"), 381 | .DIV => StackEffects.fromEffectNotation("a b -- a/b"), 382 | .AND => StackEffects.fromEffectNotation("a b -- a&b"), 383 | .ORA => StackEffects.fromEffectNotation("a b -- a|b"), 384 | .EOR => StackEffects.fromEffectNotation("a b -- a^b"), 385 | .SFT => StackEffects.fromEffectNotation("a shift8 -- c"), 386 | 387 | .EQU, .NEQ, .LTH, .GTH => StackEffects.fromEffectNotation("a b -- bool8"), 388 | 389 | .DEO => StackEffects.fromEffectNotation("v device8 --"), 390 | .DEI => StackEffects.fromEffectNotation("device8 -- v"), 391 | 392 | .INC => StackEffects.fromEffectNotation("a -- a+1"), 393 | .SWP => StackEffects.fromEffectNotation("a b -- b a"), 394 | .ROT => StackEffects.fromEffectNotation("a b c -- b c a"), 395 | 396 | .STH => StackEffects.fromEffectNotation("a -- | a"), 397 | 398 | .LDZ, .LDR => StackEffects.fromEffectNotation("addr8 -- v"), 399 | .STZ, .STR => StackEffects.fromEffectNotation("v addr8 --"), 400 | 401 | .LDA => StackEffects.fromEffectNotation("addr16 -- v"), 402 | .STA => StackEffects.fromEffectNotation("v addr16 --"), 403 | 404 | .DUP => StackEffects.fromEffectNotation("a -- a a"), 405 | .OVR => StackEffects.fromEffectNotation("a b -- a b a"), 406 | .POP => StackEffects.fromEffectNotation("a --"), 407 | .NIP => StackEffects.fromEffectNotation("a b -- b"), 408 | 409 | .JMP => StackEffects.fromEffectNotation("addr -- "), 410 | .JCN => StackEffects.fromEffectNotation("cond8 addr -- "), 411 | .JSR => StackEffects.fromEffectNotation("addr -- | ret16"), 412 | }; 413 | 414 | for (.{ 0x00, 0x20, 0x40, 0x60, 0x80, 0xa0, 0xc0, 0xe0 }) |flags| 415 | effects_r[flags | raw_instruction] = default; 416 | } 417 | } 418 | 419 | for (0.., &effects_r) |instr, *eff| { 420 | const instruction: Opcode = .fromByte(@truncate(instr)); 421 | 422 | if (instruction.shortMode()) { 423 | eff.* = eff.replaceAuto(.short); 424 | } else { 425 | eff.* = eff.replaceAuto(.byte); 426 | } 427 | 428 | if (instruction.returnMode()) { 429 | std.mem.swap( 430 | StackEffect, 431 | &eff.working_stack, 432 | &eff.return_stack, 433 | ); 434 | } 435 | 436 | if (instruction.keepMode()) { 437 | eff.* = eff.keepInputs(); 438 | } 439 | } 440 | 441 | return effects_r; 442 | } 443 | 444 | pub const effects = generateEffects(); 445 | -------------------------------------------------------------------------------- /src/lib/uxn/lib.zig: -------------------------------------------------------------------------------- 1 | pub const Cpu = @import("Cpu.zig"); 2 | //pub const Debug = @import("Debug.zig"); 3 | 4 | pub const faults_enabled = false; 5 | 6 | const std = @import("std"); 7 | 8 | const Allocator = std.mem.Allocator; 9 | 10 | pub fn loadRom(alloc: Allocator, reader: anytype) !*[Cpu.page_size]u8 { 11 | var ram_pos: u16 = 0x0100; 12 | var ram = try alloc.alloc(u8, Cpu.page_size); 13 | 14 | while (true) { 15 | const r = try reader.readAll(ram[ram_pos..ram_pos +| 0x1000]); 16 | 17 | ram_pos += @truncate(r); 18 | 19 | if (r < 0x1000) 20 | break; 21 | } 22 | 23 | // Zero out the zero-page and everything behind the ROM 24 | @memset(ram[0..0x100], 0x00); 25 | @memset(ram[ram_pos..Cpu.page_size], 0x00); 26 | 27 | return @ptrCast(ram); 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/varvara/devices/audio.zig: -------------------------------------------------------------------------------- 1 | const Cpu = @import("uxn-core").Cpu; 2 | 3 | const std = @import("std"); 4 | const logger = std.log.scoped(.uxn_varvara_audio); 5 | 6 | pub const sample_rate = 44100; 7 | pub const sample_count = 256; 8 | 9 | const timer: f32 = (@as(f32, @floatFromInt(sample_count)) / @as(f32, @floatFromInt(sample_rate))) * 1000.0; 10 | 11 | pub const AdsrFlags = packed struct(u16) { 12 | release: u4, 13 | sustain: u4, 14 | decay: u4, 15 | attack: u4, 16 | }; 17 | 18 | pub const PitchFlags = packed struct(u8) { 19 | midi_note: u7, 20 | dont_loop: bool, 21 | 22 | pub fn note(pitch: PitchFlags) u4 { 23 | return @truncate(pitch.midi_note % 12); 24 | } 25 | 26 | pub fn octave(pitch: PitchFlags) u4 { 27 | return @truncate(pitch.midi_note / 12); 28 | } 29 | }; 30 | 31 | pub const VolumeFlags = packed struct(u8) { 32 | right: u4, 33 | left: u4, 34 | }; 35 | 36 | pub const ports = struct { 37 | pub const vector = 0x0; 38 | pub const position = 0x2; 39 | pub const output = 0x4; 40 | pub const duration = 0x5; 41 | pub const adsr = 0x8; 42 | pub const length = 0xa; 43 | pub const addr = 0xc; 44 | pub const volume = 0xe; 45 | pub const pitch = 0xf; 46 | }; 47 | 48 | const Sample = @import("audio/Sample.zig"); 49 | const Envelope = @import("audio/Envelope.zig"); 50 | 51 | const base_frequencies: [12]f32 = .{ 52 | 8.1757989156, // C -1 53 | 8.6619572180, // C# -1 54 | 9.1770239974, // D -1 55 | 9.7227182413, // D# -1 56 | 10.3008611535, // E -1 57 | 10.9133822323, // F -1 58 | 11.5623257097, // F# -1 59 | 12.2498573744, // G -1 60 | 12.9782717994, // G# -1 61 | 13.7500000000, // A -1 62 | 14.5676175474, // A# -1 63 | 15.4338531643, // B -1 64 | }; 65 | 66 | fn getFrequency(pitch: PitchFlags) f32 { 67 | const pitch_exponential: f32 = @floatFromInt(@as(u32, 2) << (pitch.octave() - 1)); 68 | 69 | return base_frequencies[pitch.note()] * pitch_exponential; 70 | } 71 | 72 | fn getDuration(pitch: PitchFlags, sample: []const u8) f32 { 73 | const tone_freq = getFrequency(pitch); 74 | const base_freq = getFrequency(@bitCast(@as(u8, 60))); 75 | 76 | return @as(f32, @floatFromInt(sample.len)) / (tone_freq / base_freq); 77 | } 78 | 79 | const pitch_names: [12][:0]const u8 = .{ 80 | "C", 81 | "C♯ / D♭", 82 | "D", 83 | "D♯ / E♭", 84 | "E", 85 | "F", 86 | "F♯ / G♭", 87 | "G", 88 | "G♯ / A♭", 89 | "A", 90 | "A♯ / B♭", 91 | "B", 92 | }; 93 | 94 | pub const Audio = struct { 95 | addr: u4, 96 | 97 | vol_left: f32 = 1.0, 98 | vol_right: f32 = 1.0, 99 | 100 | active_sample: ?Sample = null, 101 | next_sample: ?Sample = null, 102 | 103 | pitch: PitchFlags = undefined, 104 | duration: f32 = 0.0, 105 | 106 | pub usingnamespace @import("impl.zig").DeviceMixin(@This()); 107 | 108 | pub fn getOutputVU(dev: *@This()) u8 { 109 | return if (dev.active_sample) |sample| 110 | @as(u8, @intFromFloat(sample.envelope.vol * 255)) 111 | else 112 | 0x00; 113 | } 114 | 115 | fn startAudio(dev: *@This(), cpu: *Cpu) void { 116 | var pitch = dev.loadPort(PitchFlags, cpu, ports.pitch); 117 | 118 | const volume = dev.loadPort(VolumeFlags, cpu, ports.volume); 119 | const adsr = dev.loadPort(AdsrFlags, cpu, ports.adsr); 120 | 121 | const duration = dev.loadPort(u16, cpu, ports.duration); 122 | const addr = dev.loadPort(u16, cpu, ports.addr); 123 | const len = dev.loadPort(u16, cpu, ports.length); 124 | 125 | const sample = cpu.mem[addr..addr +| len]; 126 | 127 | if (pitch.midi_note == 0) { 128 | return dev.stopNote(duration); 129 | } else if (pitch.midi_note < 20 or len == 0) { 130 | pitch.midi_note = 20; 131 | } 132 | 133 | dev.next_sample = dev.startNote( 134 | pitch, 135 | duration, 136 | volume, 137 | adsr, 138 | sample, 139 | ); 140 | } 141 | 142 | pub fn startNote( 143 | dev: *@This(), 144 | pitch: PitchFlags, 145 | duration: u16, 146 | volume: VolumeFlags, 147 | adsr: AdsrFlags, 148 | sample: []const u8, 149 | ) ?Sample { 150 | // Adjust the playback speed based on the sample rate and sample length, calculate 151 | // our frequency exponential for the octave. 152 | const rate_adjust: f32 = sample_rate / @as(f32, @floatFromInt(sample.len)); 153 | const tone_freq = getFrequency(pitch); 154 | 155 | const sample_data = Sample{ 156 | .data = sample, 157 | .position = 0, 158 | .loop_len = @floatFromInt(if (pitch.dont_loop) 0 else sample.len), 159 | 160 | .increment = if (sample.len <= sample_count) 161 | tone_freq / rate_adjust 162 | else 163 | tone_freq / rate_adjust / (sample_rate / 1000), 164 | 165 | .envelope = Envelope.init(timer / sample_count, adsr), 166 | }; 167 | 168 | dev.vol_left = @as(f32, @floatFromInt(volume.left)) / 15.0; 169 | dev.vol_right = @as(f32, @floatFromInt(volume.right)) / 15.0; 170 | 171 | dev.pitch = pitch; 172 | dev.duration = if (duration == 0) 173 | getDuration(pitch, sample) 174 | else 175 | @floatFromInt(duration); 176 | 177 | logger.debug("[Audio@{x}] Start playing {s} {d} ({x:0>2}); ADSR: {x:0>4}; Volume: {x:0>2}; Duration: {:.3}; SL = {x:0>4}; F = {d:.6}", .{ 178 | dev.addr, 179 | pitch_names[pitch.note()], 180 | pitch.octave(), 181 | pitch.midi_note, 182 | @as(u16, @bitCast(adsr)), 183 | @as(u8, @bitCast(volume)), 184 | dev.duration, 185 | sample.len, 186 | tone_freq, 187 | }); 188 | 189 | return sample_data; 190 | } 191 | 192 | pub fn stopNote( 193 | dev: *@This(), 194 | duration: u16, 195 | ) void { 196 | logger.debug("[Audio@{x}] Stop playing; Duration: {:.3}", .{ 197 | dev.addr, 198 | duration, 199 | }); 200 | 201 | if (dev.active_sample) |*s| { 202 | dev.duration = if (duration == 0) 203 | getDuration(@bitCast(@as(u8, 20)), s.data) 204 | else 205 | @floatFromInt(duration); 206 | 207 | s.envelope.off(); 208 | } 209 | } 210 | 211 | pub fn updateDuration(dev: *@This()) void { 212 | dev.duration -= timer; 213 | } 214 | 215 | pub fn evaluateFinishVector(dev: *@This(), cpu: *Cpu) !void { 216 | const vector = dev.loadPort(u16, cpu, ports.vector); 217 | 218 | if (vector != 0x0000) { 219 | return cpu.evaluateVector(vector); 220 | } 221 | } 222 | 223 | const crossfade_samples = 100; 224 | 225 | // Divide once at comptime so we only need to multiply below. 226 | const inv_crossfade: f32 = 1.0 / @as(f32, @floatFromInt(crossfade_samples * 2)); 227 | 228 | pub fn renderAudio(dev: *@This(), samples: []i16) void { 229 | var i: usize = 0; 230 | 231 | if (dev.next_sample) |*next_sample| { 232 | // Crossfade to next sample 233 | while (i < crossfade_samples * 2) : (i += 2) { 234 | // See how far along the cross-fade are we, from 0.0 -> 1.0 and 235 | // linearly interpolate between old and new. 236 | const f = @as(f32, @floatFromInt(i)) * inv_crossfade; 237 | 238 | const a = next_sample.getNextSample() orelse 0.0; 239 | const b = if (dev.active_sample) |*as| 240 | as.getNextSample() orelse 0.0 241 | else 242 | 0.0; 243 | 244 | const next = (f * a) + ((1.0 - f) * b); 245 | 246 | for (0.., [2]f32{ dev.vol_left, dev.vol_right }) |j, v| { 247 | samples[i + j] += @intFromFloat(next * v); 248 | } 249 | } 250 | 251 | dev.active_sample = next_sample.*; 252 | dev.next_sample = null; 253 | } 254 | 255 | if (dev.active_sample) |*active_sample| { 256 | while (i < samples.len) : (i += 2) { 257 | const sample = active_sample.getNextSample() orelse { 258 | break; 259 | }; 260 | 261 | for (0.., [2]f32{ dev.vol_left, dev.vol_right }) |j, v| { 262 | samples[i + j] += @intFromFloat(sample * v); 263 | } 264 | } 265 | } 266 | } 267 | 268 | pub fn intercept( 269 | dev: *@This(), 270 | cpu: *Cpu, 271 | port: u4, 272 | kind: Cpu.InterceptKind, 273 | ) !void { 274 | if (kind == .input) { 275 | if (port == ports.output) { 276 | dev.storePort(u8, cpu, ports.output, dev.getOutputVU()); 277 | } else if (port == ports.position or port == ports.position + 1) { 278 | dev.storePort( 279 | u16, 280 | cpu, 281 | ports.position, 282 | if (dev.active_sample) |sample| 283 | @intFromFloat(sample.position) 284 | else 285 | 0, 286 | ); 287 | } 288 | } else if (kind == .output and port == ports.pitch) { 289 | dev.startAudio(cpu); 290 | } 291 | } 292 | }; 293 | -------------------------------------------------------------------------------- /src/lib/varvara/devices/audio/Envelope.zig: -------------------------------------------------------------------------------- 1 | const AdsrFlags = @import("../audio.zig").AdsrFlags; 2 | 3 | a: f32, 4 | d: f32, 5 | s: f32, 6 | r: f32, 7 | 8 | vol: f32, 9 | 10 | stage: enum { 11 | attack, 12 | decay, 13 | sustain, 14 | release, 15 | }, 16 | 17 | pub fn init(timing: f32, adsr: AdsrFlags) @This() { 18 | const attack = @as(f32, @floatFromInt(adsr.attack)) * 64; 19 | const decay = @as(f32, @floatFromInt(adsr.decay)) * 64; 20 | const sustain = @as(f32, @floatFromInt(adsr.sustain)) / 16; 21 | const release = @as(f32, @floatFromInt(adsr.release)) * 64; 22 | 23 | var env = @This(){ 24 | .a = 0.0, 25 | .d = timing / @max(10.0, decay), 26 | .s = sustain, 27 | .r = timing / @max(10.0, release), 28 | .vol = 0.0, 29 | .stage = .attack, 30 | }; 31 | 32 | if (attack > 0) { 33 | env.a = timing / attack; 34 | } else if (env.stage == .attack) { 35 | env.stage = .decay; 36 | env.vol = 1.0; 37 | } 38 | 39 | return env; 40 | } 41 | 42 | pub fn off(env: *@This()) void { 43 | env.stage = .release; 44 | } 45 | 46 | pub fn advance(env: *@This()) void { 47 | switch (env.stage) { 48 | .attack => { 49 | env.vol += env.a; 50 | 51 | if (env.vol >= 1.0) { 52 | env.stage = .decay; 53 | env.vol = 1.0; 54 | } 55 | }, 56 | 57 | .decay => { 58 | env.vol -= env.d; 59 | 60 | if (env.vol <= env.s or env.d <= 0.0) { 61 | env.stage = .sustain; 62 | env.vol = env.s; 63 | } 64 | }, 65 | 66 | .sustain => { 67 | env.vol = env.s; 68 | }, 69 | 70 | .release => { 71 | if (env.vol <= 0.0 or env.r <= 0.0) { 72 | env.vol = 0; 73 | } else { 74 | env.vol -= env.r; 75 | } 76 | }, 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/lib/varvara/devices/audio/Sample.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const Envelope = @import("Envelope.zig"); 4 | 5 | data: []const u8, 6 | 7 | position: f32, 8 | increment: f32, 9 | loop_len: f32, 10 | 11 | envelope: Envelope, 12 | 13 | pub fn getNextSample(sample: *@This()) ?f32 { 14 | if (sample.position >= @as(f32, @floatFromInt(sample.data.len))) { 15 | if (sample.loop_len == 0) { 16 | return null; 17 | } 18 | 19 | while (sample.position >= @as(f32, @floatFromInt(sample.data.len))) { 20 | sample.position -= sample.loop_len; 21 | } 22 | } 23 | 24 | defer sample.advance(); 25 | 26 | const p1: usize = @intFromFloat(sample.position); 27 | const raw: f32 = @floatFromInt(@as(i8, @bitCast(sample.data[p1] ^ 0x80))); 28 | 29 | return raw * std.math.clamp(sample.envelope.vol, 0.0, 1.0); 30 | } 31 | 32 | fn advance(sample: *@This()) void { 33 | sample.position += sample.increment; 34 | sample.envelope.advance(); 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/varvara/devices/console.zig: -------------------------------------------------------------------------------- 1 | const Cpu = @import("uxn-core").Cpu; 2 | 3 | const std = @import("std"); 4 | const logger = std.log.scoped(.uxn_varvara_console); 5 | 6 | pub const ports = struct { 7 | pub const vector = 0x0; 8 | pub const read = 0x2; 9 | pub const typ = 0x7; 10 | pub const write = 0x8; 11 | pub const err = 0x9; 12 | }; 13 | 14 | pub const Console = struct { 15 | addr: u4, 16 | 17 | pub usingnamespace @import("impl.zig").DeviceMixin(@This()); 18 | 19 | pub fn intercept( 20 | dev: @This(), 21 | cpu: *Cpu, 22 | port: u4, 23 | kind: Cpu.InterceptKind, 24 | stdout_writer: anytype, 25 | stderr_writer: anytype, 26 | ) !void { 27 | if (kind != .output) 28 | return; 29 | 30 | if (port != ports.write and port != ports.err) 31 | return; 32 | 33 | const octet = dev.loadPort(u8, cpu, port); 34 | 35 | if (port == ports.write) { 36 | _ = stdout_writer.write(&[_]u8{octet}) catch return; 37 | } else if (port == ports.err) { 38 | _ = stderr_writer.write(&[_]u8{octet}) catch return; 39 | } 40 | } 41 | 42 | pub fn pushArguments( 43 | dev: @This(), 44 | cpu: *Cpu, 45 | args: [][]const u8, 46 | ) !void { 47 | for (0.., args) |i, arg| { 48 | for (arg) |oct| { 49 | dev.storePort(u8, cpu, ports.typ, 0x2); 50 | dev.storePort(u8, cpu, ports.read, oct); 51 | 52 | try cpu.evaluateVector(dev.loadPort(u16, cpu, ports.vector)); 53 | } 54 | 55 | dev.storePort(u8, cpu, ports.typ, if (i == args.len - 1) 0x4 else 0x3); 56 | dev.storePort(u8, cpu, ports.read, 0x10); 57 | 58 | try cpu.evaluateVector(dev.loadPort(u16, cpu, ports.vector)); 59 | } 60 | } 61 | 62 | pub fn setArgc( 63 | dev: @This(), 64 | cpu: *Cpu, 65 | args: [][]const u8, 66 | ) void { 67 | dev.storePort(u8, cpu, ports.typ, @intFromBool(args.len > 0)); 68 | } 69 | 70 | pub fn pushStdinByte( 71 | dev: @This(), 72 | cpu: *Cpu, 73 | byte: u8, 74 | ) !void { 75 | const vector = dev.loadPort(u16, cpu, ports.vector); 76 | 77 | dev.storePort(u8, cpu, ports.typ, 0x1); 78 | dev.storePort(u8, cpu, ports.read, byte); 79 | 80 | if (vector > 0x0000) 81 | try cpu.evaluateVector(vector); 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /src/lib/varvara/devices/controller.zig: -------------------------------------------------------------------------------- 1 | const Cpu = @import("uxn-core").Cpu; 2 | 3 | const std = @import("std"); 4 | const logger = std.log.scoped(.uxn_varvara_controller); 5 | 6 | pub const ButtonFlags = packed struct(u8) { 7 | ctrl: bool = false, 8 | alt: bool = false, 9 | shift: bool = false, 10 | start: bool = false, 11 | up: bool = false, 12 | down: bool = false, 13 | left: bool = false, 14 | right: bool = false, 15 | }; 16 | 17 | pub const ports = struct { 18 | pub const vector = 0x0; 19 | pub const buttons = 0x2; 20 | pub const key = 0x3; 21 | pub const p2 = 0x5; 22 | pub const p3 = 0x6; 23 | pub const p4 = 0x7; 24 | }; 25 | 26 | pub const Controller = struct { 27 | addr: u4, 28 | 29 | pub usingnamespace @import("impl.zig").DeviceMixin(@This()); 30 | 31 | pub fn intercept( 32 | dev: *@This(), 33 | cpu: *Cpu, 34 | port: u4, 35 | kind: Cpu.InterceptKind, 36 | ) !void { 37 | _ = dev; 38 | _ = cpu; 39 | _ = port; 40 | _ = kind; 41 | } 42 | 43 | fn invokeVector(dev: *@This(), cpu: *Cpu) !void { 44 | const vector = dev.loadPort(u16, cpu, ports.vector); 45 | 46 | if (vector > 0) 47 | try cpu.evaluateVector(vector); 48 | } 49 | 50 | fn getPlayerPort(player: u2) u4 { 51 | return switch (player) { 52 | 0x0 => ports.buttons, 53 | 0x1 => ports.p2, 54 | 0x2 => ports.p3, 55 | 0x3 => ports.p4, 56 | }; 57 | } 58 | 59 | pub fn pressKey(dev: *@This(), cpu: *Cpu, key: u8) !void { 60 | logger.debug("Sending key press: {x:0>2}", .{key}); 61 | 62 | dev.storePort(u8, cpu, ports.key, key); 63 | 64 | try dev.invokeVector(cpu); 65 | } 66 | 67 | pub fn pressButtons(dev: *@This(), cpu: *Cpu, buttons: ButtonFlags, player: u2) !void { 68 | const playerPort = getPlayerPort(player); 69 | 70 | const old_state = dev.loadPort(u8, cpu, playerPort); 71 | const new_state = old_state | @as(u8, @bitCast(buttons)); 72 | 73 | logger.debug("Button State: {} (Player: {})", .{ @as(ButtonFlags, @bitCast(new_state)), player }); 74 | 75 | dev.storePort(u8, cpu, playerPort, new_state); 76 | 77 | try dev.invokeVector(cpu); 78 | } 79 | 80 | pub fn releaseButtons(dev: *@This(), cpu: *Cpu, buttons: ButtonFlags, player: u2) !void { 81 | const playerPort = getPlayerPort(player); 82 | 83 | const old_state = dev.loadPort(u8, cpu, playerPort); 84 | const new_state = old_state & ~@as(u8, @bitCast(buttons)); 85 | 86 | logger.debug("Button State: {} (Player: {})", .{ @as(ButtonFlags, @bitCast(new_state)), player }); 87 | 88 | dev.storePort(u8, cpu, playerPort, new_state); 89 | 90 | try dev.invokeVector(cpu); 91 | } 92 | }; 93 | -------------------------------------------------------------------------------- /src/lib/varvara/devices/datetime.zig: -------------------------------------------------------------------------------- 1 | const Cpu = @import("uxn-core").Cpu; 2 | 3 | const ctime = @cImport({ 4 | @cInclude("time.h"); 5 | }); 6 | 7 | pub const ports = struct { 8 | pub const year = 0x0; 9 | pub const month = 0x2; 10 | pub const day = 0x3; 11 | pub const hour = 0x4; 12 | pub const minute = 0x5; 13 | pub const second = 0x6; 14 | pub const dotw = 0x7; 15 | pub const doty = 0x8; 16 | pub const isdst = 0xa; 17 | }; 18 | 19 | pub const Datetime = struct { 20 | addr: u4, 21 | 22 | localtime: bool = true, 23 | 24 | pub usingnamespace @import("impl.zig").DeviceMixin(@This()); 25 | 26 | pub fn intercept( 27 | dev: @This(), 28 | cpu: *Cpu, 29 | port: u4, 30 | kind: Cpu.InterceptKind, 31 | ) !void { 32 | if (kind != .input) 33 | return; 34 | 35 | const now = ctime.time(null); 36 | const local = if (dev.localtime) ctime.localtime(&now) else ctime.gmtime(&now); 37 | 38 | switch (port) { 39 | ports.year, ports.year + 1 => { 40 | dev.storePort(u16, cpu, ports.year, @as(u16, @intCast(local.*.tm_year + 1900))); 41 | }, 42 | ports.month => { 43 | dev.storePort(u8, cpu, ports.month, @as(u8, @intCast(local.*.tm_mon))); 44 | }, 45 | ports.day => { 46 | dev.storePort(u8, cpu, ports.day, @as(u8, @intCast(local.*.tm_mday))); 47 | }, 48 | ports.hour => { 49 | dev.storePort(u8, cpu, ports.hour, @as(u8, @intCast(local.*.tm_hour))); 50 | }, 51 | ports.minute => { 52 | dev.storePort(u8, cpu, ports.minute, @as(u8, @intCast(local.*.tm_min))); 53 | }, 54 | ports.second => { 55 | dev.storePort(u8, cpu, ports.second, @as(u8, @intCast(local.*.tm_sec))); 56 | }, 57 | ports.dotw => { 58 | dev.storePort(u8, cpu, ports.dotw, @as(u8, @intCast(local.*.tm_wday))); 59 | }, 60 | ports.doty, ports.doty + 1 => { 61 | dev.storePort(u16, cpu, ports.doty, @as(u8, @intCast(local.*.tm_yday))); 62 | }, 63 | ports.isdst => { 64 | dev.storePort(u8, cpu, ports.isdst, @as(u8, @intCast(local.*.tm_isdst))); 65 | }, 66 | 67 | else => {}, 68 | } 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /src/lib/varvara/devices/file.zig: -------------------------------------------------------------------------------- 1 | const Cpu = @import("uxn-core").Cpu; 2 | 3 | const builtin = @import("builtin"); 4 | const std = @import("std"); 5 | const logger = std.log.scoped(.uxn_varvara_file); 6 | 7 | pub const ports = struct { 8 | pub const vector = 0x0; 9 | pub const success = 0x2; 10 | pub const stat = 0x4; 11 | pub const delete = 0x6; 12 | pub const append = 0x7; 13 | pub const name = 0x8; 14 | pub const length = 0xa; 15 | pub const read = 0xc; 16 | pub const write = 0xe; 17 | }; 18 | 19 | pub const File = struct { 20 | const Impl = if (builtin.target.os.tag != .freestanding) 21 | @import("fs/default.zig").Impl(@This()) 22 | else 23 | @import("fs/noop.zig").Impl(@This()); 24 | 25 | addr: u4, 26 | active_file: ?Impl.Wrapper = null, 27 | impl: Impl = .{}, 28 | 29 | pub usingnamespace @import("impl.zig").DeviceMixin(@This()); 30 | pub usingnamespace Impl; 31 | 32 | pub fn cleanup(dev: *@This()) void { 33 | if (dev.active_file) |*f| { 34 | f.close(); 35 | 36 | logger.debug("[File@{x}] Closed previousely open target", .{ 37 | dev.addr, 38 | }); 39 | } 40 | 41 | dev.active_file = null; 42 | } 43 | 44 | fn getPortSlice(dev: *@This(), cpu: *Cpu, comptime port: comptime_int) []u8 { 45 | const ptr: usize = dev.loadPort(u16, cpu, port); 46 | 47 | return if (port == ports.name) 48 | std.mem.sliceTo(cpu.mem[ptr..], 0x00) 49 | else 50 | return cpu.mem[ptr..ptr +| dev.loadPort(u16, cpu, ports.length)]; 51 | } 52 | 53 | pub fn intercept( 54 | dev: *@This(), 55 | cpu: *Cpu, 56 | port: u4, 57 | kind: Cpu.InterceptKind, 58 | ) !void { 59 | if (kind != .output) 60 | return; 61 | 62 | switch (port) { 63 | ports.name + 1 => { 64 | // Close a previously opened file 65 | dev.cleanup(); 66 | dev.storePort(u16, cpu, ports.success, 0x0001); 67 | }, 68 | 69 | ports.write + 1 => { 70 | const truncate = dev.loadPort(u8, cpu, ports.append) == 0x00; 71 | 72 | const name_slice = dev.getPortSlice(cpu, ports.name); 73 | const data_slice = dev.getPortSlice(cpu, ports.write); 74 | 75 | var t = dev.active_file orelse dev.openWritable(name_slice, truncate) catch |err| { 76 | logger.debug("[File@{x}] Failed opening \"{s}\" for {s} access: {}", .{ 77 | dev.addr, 78 | name_slice, 79 | if (truncate) "write" else "append", 80 | err, 81 | }); 82 | 83 | return dev.storePort(u16, cpu, ports.success, 0x0000); 84 | }; 85 | 86 | if (dev.active_file == null) { 87 | logger.debug("[File@{x}] Opened \"{s}\" for {s} access", .{ 88 | dev.addr, 89 | name_slice, 90 | if (truncate) "write" else "append", 91 | }); 92 | } 93 | 94 | const res: u16 = if (t.write(data_slice)) |n| r: { 95 | logger.debug("[File@{x}] Wrote {} bytes", .{ dev.addr, n }); 96 | 97 | break :r n; 98 | } else |err| r: { 99 | logger.debug("[File@{x}] Failed to write data: {}", .{ dev.addr, err }); 100 | 101 | break :r 0x0000; 102 | }; 103 | 104 | dev.storePort(u16, cpu, ports.success, res); 105 | dev.active_file = t; 106 | }, 107 | 108 | ports.read + 1 => { 109 | const name_slice = dev.getPortSlice(cpu, ports.name); 110 | const data_slice = dev.getPortSlice(cpu, ports.read); 111 | 112 | var t = dev.active_file orelse dev.openReadable(name_slice) catch |err| { 113 | logger.debug("[File@{x}] Failed opening \"{s}\" for read access: {}", .{ dev.addr, name_slice, err }); 114 | 115 | return dev.storePort(u16, cpu, ports.success, 0x0000); 116 | }; 117 | 118 | if (dev.active_file == null) { 119 | logger.debug("[File@{x}] Opened \"{s}\" for read access", .{ dev.addr, name_slice }); 120 | } 121 | 122 | const res: u16 = if (t.read(data_slice)) |n| r: { 123 | logger.debug("[File@{x}] Read {} bytes", .{ dev.addr, n }); 124 | 125 | break :r n; 126 | } else |err| r: { 127 | logger.debug("[File@{x}] Failed to read data: {}", .{ dev.addr, err }); 128 | 129 | break :r 0x0000; 130 | }; 131 | 132 | dev.storePort(u16, cpu, ports.success, res); 133 | dev.active_file = t; 134 | }, 135 | 136 | ports.delete => { 137 | const name_slice = dev.getPortSlice(cpu, ports.name); 138 | 139 | const res: u16 = if (dev.deleteFile(name_slice)) r: { 140 | logger.debug("[File@{x}] Deleted \"{s}\"", .{ dev.addr, name_slice }); 141 | 142 | break :r 0x0000; 143 | } else |err| r: { 144 | logger.debug("[File@{x}] Failed deleting \"{s}\": {}", .{ dev.addr, name_slice, err }); 145 | 146 | break :r 0x0000; 147 | }; 148 | 149 | dev.storePort(u16, cpu, ports.success, res); 150 | }, 151 | 152 | ports.stat + 1 => { 153 | const name_slice = dev.getPortSlice(cpu, ports.name); 154 | 155 | logger.warn("[File@{x}] Called stat on \"{s}\"; not implemented", .{ dev.addr, name_slice }); 156 | 157 | dev.storePort(u16, cpu, ports.success, 0x0000); 158 | }, 159 | 160 | else => {}, 161 | } 162 | } 163 | }; 164 | -------------------------------------------------------------------------------- /src/lib/varvara/devices/fs/default.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const std = @import("std"); 3 | const fs = std.fs; 4 | const io = std.io; 5 | 6 | const Directory = struct { 7 | root: fs.Dir, 8 | iter: fs.Dir.Iterator, 9 | 10 | cached_entry: ?fs.Dir.Entry = null, 11 | 12 | fn renderDirEntry( 13 | dir: *Directory, 14 | entry: fs.Dir.Entry, 15 | slice: []u8, 16 | ) !usize { 17 | var fbw = io.FixedBufferStream([]u8){ 18 | .buffer = slice, 19 | .pos = 0, 20 | }; 21 | 22 | var writer = fbw.writer(); 23 | 24 | if (entry.kind != .directory) { 25 | const file_size = if (comptime builtin.os.tag == .wasi) s: { 26 | // Some problems with statFile not working under WASI 27 | if (dir.root.dir.openFile(entry.name, .{})) |f| { 28 | defer f.close(); 29 | 30 | break :s f.getEndPos() catch 0x10000; 31 | } else |_| { 32 | break :s 0x10000; 33 | } 34 | } else (try dir.root.statFile(entry.name)).size; 35 | 36 | try if (file_size > 0xffff) 37 | writer.print("???? {s}\n", .{entry.name}) 38 | else 39 | writer.print("{x:0>4} {s}\n", .{ file_size, entry.name }); 40 | } else { 41 | try writer.print("---- {s}/\n", .{entry.name}); 42 | } 43 | 44 | return fbw.pos; 45 | } 46 | }; 47 | 48 | pub fn Impl(comptime Self: type) type { 49 | return struct { 50 | pub const Mode = enum { 51 | read, 52 | write, 53 | append, 54 | delete, 55 | }; 56 | 57 | access_filter: ?*const fn ( 58 | dev: *Self, 59 | data: ?*anyopaque, 60 | path: []const u8, 61 | access_type: Mode, 62 | ) bool = null, 63 | 64 | access_filter_arg: ?*anyopaque = null, 65 | 66 | pub const Wrapper = ImplWrapper; 67 | 68 | pub fn openReadable(dev: *Self, path: []const u8) !Wrapper { 69 | if (dev.impl.access_filter) |filter| { 70 | if (!filter(dev, dev.impl.access_filter_arg, path, .read)) 71 | return error.Sandboxed; 72 | } 73 | 74 | if (fs.cwd().openDir(path, .{ .iterate = true })) |dir| { 75 | return .{ 76 | .directory = .{ 77 | .root = dir, 78 | .iter = dir.iterate(), 79 | }, 80 | }; 81 | } else |err| { 82 | if (err == error.NotDir) { 83 | return .{ 84 | .file = try fs.cwd().openFile(path, .{}), 85 | }; 86 | } 87 | } 88 | 89 | return error.CannotOpen; 90 | } 91 | 92 | pub fn setAccessFilter( 93 | dev: *Self, 94 | context: anytype, 95 | filter_fun: *const fn ( 96 | dev: *Self, 97 | data: ?*anyopaque, 98 | path: []const u8, 99 | access_type: Mode, 100 | ) bool, 101 | ) void { 102 | dev.impl.access_filter = filter_fun; 103 | dev.impl.access_filter_arg = context; 104 | } 105 | 106 | pub fn openWritable(dev: *Self, path: []const u8, truncate: bool) !Wrapper { 107 | if (dev.impl.access_filter) |filter| { 108 | if (!filter(dev, dev.impl.access_filter_arg, path, if (truncate) .write else .append)) 109 | return error.Sandboxed; 110 | } 111 | 112 | return .{ 113 | .file = try fs.cwd().createFile(path, .{ .truncate = truncate }), 114 | }; 115 | } 116 | 117 | pub fn deleteFile(dev: *Self, path: []const u8) !void { 118 | if (dev.impl.access_filter) |filter| { 119 | if (!filter(dev, dev.impl.access_filter_arg, path, .delete)) 120 | return error.Sandboxed; 121 | } 122 | 123 | try fs.cwd().deleteFile(path); 124 | } 125 | }; 126 | } 127 | 128 | pub const ImplWrapper = union(enum) { 129 | directory: Directory, 130 | file: fs.File, 131 | 132 | pub fn read(self: *@This(), buf: []u8) !u16 { 133 | switch (self.*) { 134 | .directory => |*dir| { 135 | var offset: usize = 0; 136 | 137 | if (dir.cached_entry) |entry| { 138 | offset += dir.renderDirEntry(entry, buf[offset..]) catch 0; 139 | 140 | dir.cached_entry = null; 141 | } 142 | 143 | while (dir.iter.next() catch null) |entry| { 144 | if (dir.renderDirEntry(entry, buf[offset..])) |written| { 145 | offset += written; 146 | } else |err| { 147 | if (err == error.NoSpaceLeft) { 148 | // If we cannot write the current entry, we rember it for the next call. 149 | dir.cached_entry = entry; 150 | 151 | break; 152 | } else { 153 | return err; 154 | } 155 | } 156 | } 157 | 158 | @memset(buf[offset..], 0x00); 159 | 160 | return @truncate(offset); 161 | }, 162 | 163 | .file => |*f| { 164 | return @truncate(try f.readAll(buf)); 165 | }, 166 | } 167 | } 168 | 169 | pub fn write(self: *@This(), buf: []const u8) !u16 { 170 | return switch (self.*) { 171 | .file => |f| { 172 | return @truncate(try f.write(buf)); 173 | }, 174 | 175 | else => error.NotImplemented, 176 | }; 177 | } 178 | 179 | pub fn close(self: *@This()) void { 180 | switch (self.*) { 181 | .directory => |*dir| dir.root.close(), 182 | .file => |*file| file.close(), 183 | } 184 | } 185 | }; 186 | -------------------------------------------------------------------------------- /src/lib/varvara/devices/fs/noop.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn Impl(comptime Self: type) type { 4 | return struct { 5 | pub const Wrapper = NoopWrapper; 6 | 7 | pub fn openReadable(dev: *Self, path: []const u8) !Wrapper { 8 | _ = dev; 9 | _ = path; 10 | 11 | return error.NotImplemented; 12 | } 13 | 14 | pub fn openWritable(dev: *Self, path: []const u8, truncate: bool) !Wrapper { 15 | _ = dev; 16 | _ = path; 17 | _ = truncate; 18 | 19 | return error.NotImplemented; 20 | } 21 | 22 | pub fn deleteFile(dev: *Self, path: []const u8) !void { 23 | _ = dev; 24 | _ = path; 25 | 26 | return error.NotImplemented; 27 | } 28 | }; 29 | } 30 | 31 | const NoopWrapper = struct { 32 | pub fn read(self: *@This(), buf: []u8) !u16 { 33 | _ = self; 34 | _ = buf; 35 | 36 | unreachable; 37 | } 38 | 39 | pub fn write(self: *@This(), buf: []const u8) !u16 { 40 | _ = self; 41 | _ = buf; 42 | 43 | unreachable; 44 | } 45 | 46 | pub fn close(self: *@This()) void { 47 | _ = self; 48 | 49 | unreachable; 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /src/lib/varvara/devices/impl.zig: -------------------------------------------------------------------------------- 1 | const Cpu = @import("uxn-core").Cpu; 2 | 3 | pub fn DeviceMixin(comptime Self: type) type { 4 | return struct { 5 | pub fn portAddress(dev: *const Self, port: u4) u8 { 6 | return @as(u8, dev.addr) << 4 | port; 7 | } 8 | 9 | pub inline fn loadPort( 10 | dev: *const Self, 11 | comptime T: type, 12 | cpu: *const Cpu, 13 | port: u4, 14 | ) T { 15 | return cpu.loadDeviceMem(T, dev.portAddress(port)); 16 | } 17 | 18 | pub inline fn storePort( 19 | dev: *const Self, 20 | comptime T: type, 21 | cpu: *Cpu, 22 | port: u4, 23 | value: T, 24 | ) void { 25 | cpu.storeDeviceMem(T, dev.portAddress(port), value); 26 | } 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/varvara/devices/mouse.zig: -------------------------------------------------------------------------------- 1 | const Cpu = @import("uxn-core").Cpu; 2 | 3 | const std = @import("std"); 4 | const logger = std.log.scoped(.uxn_varvara_mouse); 5 | 6 | pub const ButtonFlags = packed struct(u8) { 7 | left: bool, 8 | middle: bool, 9 | right: bool, 10 | _unused: u5, 11 | }; 12 | 13 | pub const ports = struct { 14 | pub const vector = 0x0; 15 | pub const x = 0x2; 16 | pub const y = 0x4; 17 | pub const state = 0x6; 18 | pub const scroll_x = 0xa; 19 | pub const scroll_y = 0xc; 20 | }; 21 | 22 | pub const Mouse = struct { 23 | addr: u4, 24 | 25 | pub usingnamespace @import("impl.zig").DeviceMixin(@This()); 26 | 27 | pub fn intercept( 28 | dev: @This(), 29 | cpu: *Cpu, 30 | port: u4, 31 | kind: Cpu.InterceptKind, 32 | ) !void { 33 | _ = dev; 34 | _ = cpu; 35 | _ = port; 36 | _ = kind; 37 | } 38 | 39 | fn invokeVector(dev: *@This(), cpu: *Cpu) !void { 40 | const vector = dev.loadPort(u16, cpu, ports.vector); 41 | 42 | if (vector > 0) 43 | try cpu.evaluateVector(vector); 44 | } 45 | 46 | pub fn pressButtons(dev: *@This(), cpu: *Cpu, buttons: ButtonFlags) !void { 47 | const old_state = dev.loadPort(u8, cpu, ports.state); 48 | const new_state = old_state | @as(u8, @bitCast(buttons)); 49 | 50 | logger.debug("Button State: {}", .{@as(ButtonFlags, @bitCast(new_state))}); 51 | 52 | dev.storePort(u8, cpu, ports.state, new_state); 53 | 54 | try dev.invokeVector(cpu); 55 | } 56 | 57 | pub fn releaseButtons(dev: *@This(), cpu: *Cpu, buttons: ButtonFlags) !void { 58 | const old_state = dev.loadPort(u8, cpu, ports.state); 59 | const new_state = old_state & ~@as(u8, @bitCast(buttons)); 60 | 61 | logger.debug("Button State: {}", .{@as(ButtonFlags, @bitCast(new_state))}); 62 | 63 | dev.storePort(u8, cpu, ports.state, new_state); 64 | 65 | try dev.invokeVector(cpu); 66 | } 67 | 68 | pub fn updatePosition(dev: *@This(), cpu: *Cpu, x: u16, y: u16) !void { 69 | logger.debug("Set position: X: {}; Y: {}", .{ x, y }); 70 | 71 | dev.storePort(u16, cpu, ports.x, x); 72 | dev.storePort(u16, cpu, ports.y, y); 73 | 74 | try dev.invokeVector(cpu); 75 | } 76 | 77 | pub fn updateScroll(dev: *@This(), cpu: *Cpu, x: i32, y: i32) !void { 78 | logger.debug("Scrolling: X: {}; Y: {}", .{ x, -y }); 79 | 80 | dev.storePort(i16, cpu, ports.scroll_x, @as(i16, @truncate(x))); 81 | dev.storePort(i16, cpu, ports.scroll_y, @as(i16, @truncate(-y))); 82 | 83 | defer { 84 | dev.storePort(u16, cpu, ports.scroll_x, 0); 85 | dev.storePort(u16, cpu, ports.scroll_y, 0); 86 | } 87 | 88 | try dev.invokeVector(cpu); 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /src/lib/varvara/devices/screen.zig: -------------------------------------------------------------------------------- 1 | const Cpu = @import("uxn-core").Cpu; 2 | 3 | const std = @import("std"); 4 | const logger = std.log.scoped(.uxn_varvara_screen); 5 | 6 | const default_window_width = 512; 7 | const default_window_height = 320; 8 | 9 | pub const Rect = struct { 10 | x0: usize, 11 | y0: usize, 12 | x1: usize, 13 | y1: usize, 14 | }; 15 | 16 | pub const AutoFlags = packed struct(u8) { 17 | x: bool, 18 | y: bool, 19 | addr: bool, 20 | _: u1 = 0x0, 21 | add_length: u4, 22 | }; 23 | 24 | pub const PixelFlags = packed struct(u8) { 25 | color: u2, 26 | _: u2, 27 | flip_x: bool, 28 | flip_y: bool, 29 | layer: u1, 30 | fill: bool, 31 | }; 32 | 33 | pub const SpriteFlags = packed struct(u8) { 34 | blending: u4, 35 | flip_x: bool, 36 | flip_y: bool, 37 | layer: u1, 38 | two_bpp: bool, 39 | }; 40 | 41 | const blending: [4][16]u2 = .{ 42 | .{ 0, 0, 0, 0, 1, 0, 1, 1, 2, 2, 0, 2, 3, 3, 3, 0 }, 43 | .{ 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3 }, 44 | .{ 1, 2, 3, 1, 1, 2, 3, 1, 1, 2, 3, 1, 1, 2, 3, 1 }, 45 | .{ 2, 3, 1, 2, 2, 3, 1, 2, 2, 3, 1, 2, 2, 3, 1, 2 }, 46 | }; 47 | 48 | pub const ports = struct { 49 | pub const vector = 0x0; 50 | pub const width = 0x2; 51 | pub const height = 0x4; 52 | pub const auto = 0x6; 53 | pub const x = 0x8; 54 | pub const y = 0xa; 55 | pub const addr = 0xc; 56 | pub const pixel = 0xe; 57 | pub const sprite = 0xf; 58 | }; 59 | 60 | pub const Screen = struct { 61 | // Public 62 | addr: u4, 63 | 64 | width: u16 = default_window_width, 65 | height: u16 = default_window_height, 66 | 67 | dirty_region: ?Rect = null, 68 | 69 | foreground: []u2 = undefined, 70 | background: []u2 = undefined, 71 | 72 | // "Private" 73 | alloc: std.mem.Allocator, 74 | 75 | pub usingnamespace @import("impl.zig").DeviceMixin(@This()); 76 | 77 | fn normalizeRegion( 78 | dev: *@This(), 79 | region: *Rect, 80 | ) void { 81 | var x0: u16 = @truncate(region.x0); 82 | var y0: u16 = @truncate(region.y0); 83 | const x1: u16 = @truncate(region.x1); 84 | const y1: u16 = @truncate(region.y1); 85 | 86 | if (x0 > x1) x0 = 0; 87 | if (y0 > y1) y0 = 0; 88 | 89 | region.x0 = @min(dev.width, x0); 90 | region.y0 = @min(dev.height, y0); 91 | region.x1 = @min(dev.width, x1); 92 | region.y1 = @min(dev.height, y1); 93 | } 94 | 95 | fn updateDirtyRegion( 96 | dev: *@This(), 97 | x0: usize, 98 | y0: usize, 99 | x1: usize, 100 | y1: usize, 101 | ) void { 102 | //if (dev.dirty_region) |*region| { 103 | // if (x0 < region.x0) region.x0 = x0; 104 | // if (y0 < region.y0) region.y0 = y0; 105 | // if (x1 > region.x1) region.x1 = x1; 106 | // if (y1 > region.y1) region.y1 = y1; 107 | // 108 | // dev.normalize_region(region); 109 | //} else { 110 | // var region: Rect = .{ 111 | // .x0 = x0, 112 | // .y0 = y0, 113 | // .x1 = x1, 114 | // .y1 = y1, 115 | // }; 116 | // 117 | // dev.normalize_region(®ion); 118 | // 119 | // dev.dirty_region = region; 120 | //} 121 | 122 | _ = x0; 123 | _ = y0; 124 | _ = x1; 125 | _ = y1; 126 | 127 | dev.forceRedraw(); 128 | } 129 | 130 | pub fn intercept( 131 | dev: *@This(), 132 | cpu: *Cpu, 133 | port: u4, 134 | kind: Cpu.InterceptKind, 135 | ) !void { 136 | if (kind == .input) { 137 | switch (port) { 138 | ports.width, ports.width + 1 => { 139 | dev.storePort(u16, cpu, ports.width, dev.width); 140 | }, 141 | 142 | ports.height, ports.height + 1 => { 143 | dev.storePort(u16, cpu, ports.height, dev.height); 144 | }, 145 | 146 | else => {}, 147 | } 148 | } else { 149 | switch (port) { 150 | ports.width + 1 => dev.width = dev.loadPort(u16, cpu, ports.width), 151 | ports.height + 1 => dev.height = dev.loadPort(u16, cpu, ports.height), 152 | 153 | ports.pixel => { 154 | const flags = dev.loadPort(PixelFlags, cpu, ports.pixel); 155 | const auto = dev.loadPort(AutoFlags, cpu, ports.auto); 156 | 157 | var x0 = dev.loadPort(u16, cpu, ports.x); 158 | var y0 = dev.loadPort(u16, cpu, ports.y); 159 | 160 | var x1: u16 = undefined; 161 | var y1: u16 = undefined; 162 | 163 | const layer = if (flags.layer == 0x00) dev.background else dev.foreground; 164 | 165 | if (flags.fill) { 166 | x1 = if (flags.flip_x) 0 else dev.width; 167 | y1 = if (flags.flip_y) 0 else dev.height; 168 | 169 | if (x0 > x1) std.mem.swap(u16, &x0, &x1); 170 | if (y0 > y1) std.mem.swap(u16, &y0, &y1); 171 | 172 | dev.fillRegion(layer, x0, y0, x1, y1, flags); 173 | } else { 174 | x1 = x0 +% 1; 175 | y1 = y0 +% 1; 176 | 177 | if (x0 < dev.width and y0 < dev.height) 178 | layer[@as(usize, y0) * dev.width + x0] = flags.color; 179 | 180 | if (auto.x) dev.storePort(u16, cpu, ports.x, x1); 181 | if (auto.y) dev.storePort(u16, cpu, ports.y, y1); 182 | } 183 | 184 | dev.updateDirtyRegion(x0, y0, x1, y1); 185 | }, 186 | 187 | ports.sprite => { 188 | const flags = dev.loadPort(SpriteFlags, cpu, ports.sprite); 189 | const auto = dev.loadPort(AutoFlags, cpu, ports.auto); 190 | 191 | const x = dev.loadPort(i16, cpu, ports.x); 192 | const y = dev.loadPort(i16, cpu, ports.y); 193 | 194 | const dx: i16 = if (auto.x) 8 else 0; 195 | const dy: i16 = if (auto.y) 8 else 0; 196 | 197 | const fx: i16 = if (flags.flip_x) -1 else 1; 198 | const fy: i16 = if (flags.flip_y) -1 else 1; 199 | 200 | const da: u16 = if (auto.addr) if (flags.two_bpp) 16 else 8 else 0; 201 | const l: u8 = @as(u8, auto.add_length) + 1; 202 | 203 | const layer = if (flags.layer == 0x00) dev.background else dev.foreground; 204 | 205 | var addr = dev.loadPort(u16, cpu, ports.addr); 206 | 207 | for (0..l) |i| { 208 | const ic: i16 = @intCast(i); 209 | 210 | // dy and dx flipped in original implementation 211 | dev.renderSprite( 212 | cpu, 213 | layer, 214 | flags, 215 | @bitCast(x +% (dy * fx * ic)), 216 | @bitCast(y +% (dx * fy * ic)), 217 | addr, 218 | ); 219 | 220 | addr +%= da; 221 | } 222 | 223 | dev.updateDirtyRegion( 224 | @as(u16, @bitCast(x)), 225 | @as(u16, @bitCast(y)), 226 | @as(u16, @truncate(@as(usize, @bitCast(@as(isize, x) +% (dy * fx * l) +% 8)))), 227 | @as(u16, @truncate(@as(usize, @bitCast(@as(isize, y) +% (dx * fy * l) +% 8)))), 228 | ); 229 | 230 | if (auto.x) dev.storePort(i16, cpu, ports.x, x +% dx * fx); 231 | if (auto.y) dev.storePort(i16, cpu, ports.y, y +% dy * fy); 232 | if (auto.addr) dev.storePort(u16, cpu, ports.addr, addr); 233 | }, 234 | 235 | else => { 236 | return; 237 | }, 238 | } 239 | 240 | if (port == ports.width + 1 or port == ports.height + 1) { 241 | dev.cleanupGraphics(); 242 | dev.initializeGraphics() catch unreachable; 243 | } 244 | } 245 | } 246 | 247 | fn renderSprite( 248 | dev: *@This(), 249 | cpu: *Cpu, 250 | layer: []u2, 251 | flags: SpriteFlags, 252 | x0: u16, 253 | y0: u16, 254 | addr: u16, 255 | ) void { 256 | const opaq = flags.blending % 5 != 0; 257 | 258 | var y: u16 = 0; 259 | 260 | while (y < 8) : (y += 1) { 261 | const c1 = cpu.mem[addr +% y]; 262 | const c2 = if (flags.two_bpp) cpu.mem[addr +% (y +% 8)] else 0; 263 | 264 | var x: u16 = 0; 265 | 266 | while (x < 8) : (x += 1) { 267 | const ch = ((c1 >> @truncate(x)) & 1) | (((c2 >> @truncate(x)) << 1) & 2); 268 | 269 | const yr = y0 +% (if (flags.flip_y) 7 - y else y); 270 | const xr = x0 +% (if (flags.flip_x) x else 7 - x); 271 | 272 | if (opaq or ch != 0x0000) { 273 | if (xr < dev.width and yr < dev.height) 274 | layer[@as(usize, yr) * dev.width + xr] = blending[ch][flags.blending]; 275 | } 276 | } 277 | } 278 | } 279 | 280 | fn fillRegion( 281 | dev: *@This(), 282 | layer: []u2, 283 | x0: u16, 284 | y0: u16, 285 | x1: u16, 286 | y1: u16, 287 | flags: PixelFlags, 288 | ) void { 289 | var y = y0; 290 | 291 | while (y < y1) : (y += 1) { 292 | var x = x0; 293 | 294 | while (x < x1) : (x += 1) { 295 | layer[@as(usize, y) * dev.width + x] = flags.color; 296 | } 297 | } 298 | } 299 | 300 | pub fn forceRedraw(dev: *@This()) void { 301 | dev.dirty_region = .{ 302 | .x0 = 0, 303 | .y0 = 0, 304 | .x1 = dev.width, 305 | .y1 = dev.height, 306 | }; 307 | } 308 | 309 | pub fn initializeGraphics(dev: *@This()) !void { 310 | logger.debug("Initialize framebuffers ({}x{})", .{ dev.width, dev.height }); 311 | 312 | dev.foreground = try dev.alloc.alloc(u2, @as(usize, dev.width) * dev.height); 313 | errdefer dev.alloc.free(dev.foreground); 314 | 315 | dev.background = try dev.alloc.alloc(u2, @as(usize, dev.width) * dev.height); 316 | errdefer dev.alloc.free(dev.background); 317 | 318 | @memset(dev.foreground, 0x00); 319 | @memset(dev.background, 0x00); 320 | 321 | dev.forceRedraw(); 322 | } 323 | 324 | pub fn cleanupGraphics(dev: *@This()) void { 325 | logger.debug("Destroying framebuffers", .{}); 326 | 327 | dev.alloc.free(dev.foreground); 328 | dev.alloc.free(dev.background); 329 | } 330 | 331 | pub fn evaluateFrame(dev: *@This(), cpu: *Cpu) !void { 332 | const vector = dev.loadPort(u16, cpu, ports.vector); 333 | 334 | if (vector != 0x0000) 335 | return cpu.evaluateVector(vector); 336 | } 337 | }; 338 | -------------------------------------------------------------------------------- /src/lib/varvara/devices/system.zig: -------------------------------------------------------------------------------- 1 | const Cpu = @import("uxn-core").Cpu; 2 | 3 | const std = @import("std"); 4 | const logger = std.log.scoped(.uxn_varvara_system); 5 | 6 | pub const Color = struct { 7 | r: u8, 8 | g: u8, 9 | b: u8, 10 | }; 11 | 12 | pub const ports = struct { 13 | pub const catch_vector = 0x00; 14 | pub const expansion = 0x02; 15 | pub const wsp = 0x04; 16 | pub const rsp = 0x05; 17 | pub const metadata = 0x06; 18 | pub const red = 0x08; 19 | pub const green = 0x0a; 20 | pub const blue = 0x0c; 21 | pub const debug = 0x0e; 22 | pub const state = 0x0f; 23 | }; 24 | 25 | pub const System = struct { 26 | addr: u4, 27 | 28 | debug_callback: ?*const fn (cpu: *Cpu, data: ?*anyopaque) void = null, 29 | callback_data: ?*anyopaque = null, 30 | additional_pages: ?[][Cpu.page_size]u8 = null, 31 | 32 | exit_code: ?u8 = null, 33 | colors: [4]Color = .{ 34 | .{ .r = 0, .g = 0, .b = 0 }, 35 | .{ .r = 0, .g = 0, .b = 0 }, 36 | .{ .r = 0, .g = 0, .b = 0 }, 37 | .{ .r = 0, .g = 0, .b = 0 }, 38 | }, 39 | 40 | pub usingnamespace @import("impl.zig").DeviceMixin(@This()); 41 | 42 | fn splitRgb(r: u16, g: u16, b: u16, c: u2) Color { 43 | const sw = @as(u4, 3 - c) * 4; 44 | 45 | return Color{ 46 | .r = @truncate((r >> sw) & 0xf | ((r >> sw) & 0xf) << 4), 47 | .g = @truncate((g >> sw) & 0xf | ((g >> sw) & 0xf) << 4), 48 | .b = @truncate((b >> sw) & 0xf | ((b >> sw) & 0xf) << 4), 49 | }; 50 | } 51 | 52 | pub fn intercept( 53 | dev: *@This(), 54 | cpu: *Cpu, 55 | port: u4, 56 | kind: Cpu.InterceptKind, 57 | ) !void { 58 | if (kind == .input) { 59 | switch (port) { 60 | ports.wsp => dev.storePort(u8, cpu, ports.wsp, cpu.wst.sp), 61 | ports.rsp => dev.storePort(u8, cpu, ports.rsp, cpu.rst.sp), 62 | 63 | else => {}, 64 | } 65 | } else { 66 | switch (port) { 67 | ports.state => { 68 | dev.exit_code = dev.loadPort(u8, cpu, ports.state) & 0x7f; 69 | 70 | logger.debug("System exit requested (code = {?})", .{dev.exit_code}); 71 | }, 72 | 73 | ports.wsp => cpu.wst.sp = dev.loadPort(u8, cpu, ports.wsp), 74 | ports.rsp => cpu.rst.sp = dev.loadPort(u8, cpu, ports.rsp), 75 | 76 | ports.debug => { 77 | if (dev.debug_callback) |cb| 78 | cb(cpu, dev.callback_data) 79 | else 80 | logger.debug("Debug port triggered, but no callback is available", .{}); 81 | }, 82 | 83 | ports.expansion + 1 => { 84 | try dev.handleExpansion(cpu, dev.loadPort(u16, cpu, ports.expansion)); 85 | }, 86 | 87 | ports.red + 1, ports.green + 1, ports.blue + 1 => { 88 | // Layout: 89 | // R 0xABCD 90 | // G 0xEFGH 91 | // B 0xIJKL => 0xAEI 0xBFJ 0xCGK 0xDHL 92 | const r = dev.loadPort(u16, cpu, ports.red); 93 | const g = dev.loadPort(u16, cpu, ports.green); 94 | const b = dev.loadPort(u16, cpu, ports.blue); 95 | 96 | for (0..4) |i| 97 | dev.colors[i] = splitRgb(r, g, b, @truncate(i)); 98 | }, 99 | 100 | else => {}, 101 | } 102 | } 103 | } 104 | 105 | pub fn handleFault(dev: *@This(), cpu: *Cpu, fault: Cpu.SystemFault) !void { 106 | const catch_vector = dev.loadPort(u16, cpu, ports.catch_vector); 107 | 108 | if (catch_vector > 0x0000 and Cpu.isCatchable(fault)) { 109 | // Clear stacks, push fault information 110 | cpu.wst.sp = 0; 111 | cpu.rst.sp = 0; 112 | 113 | cpu.wst.push(u16, cpu.pc) catch unreachable; 114 | cpu.wst.push(u8, cpu.mem[cpu.pc]) catch unreachable; 115 | cpu.wst.push(u8, @as(u8, switch (fault) { 116 | error.StackUnderflow => 0x01, 117 | error.StackOverflow => 0x02, 118 | error.DivisionByZero => 0x03, 119 | 120 | else => unreachable, 121 | })) catch unreachable; 122 | 123 | // Due to some weird effects of "usingnamespace" above, handle_fault() no longer feels 124 | // like resolving itself in a recursive call, so we make a little indirection via 125 | // @call() to help the resolver. 126 | cpu.evaluateVector(catch_vector) catch |new_fault| 127 | try @call(.auto, handleFault, .{ dev, cpu, new_fault }); 128 | } else { 129 | return fault; 130 | } 131 | } 132 | 133 | fn selectMemoryPage(dev: *@This(), cpu: *Cpu, page: u16) ?*[Cpu.page_size]u8 { 134 | if (page == 0x0000) { 135 | return cpu.mem; 136 | } else if (dev.additional_pages) |page_table| { 137 | if (page_table.len < page) { 138 | return &page_table[page]; 139 | } 140 | } 141 | 142 | return null; 143 | } 144 | 145 | fn getPagedSlice(dev: *@This(), cpu: *Cpu, page: u16, offset: u16, len: u16) ?[]u8 { 146 | const src = dev.selectMemoryPage(cpu, page) orelse { 147 | return null; 148 | }; 149 | 150 | return src[offset..offset +| len]; 151 | } 152 | 153 | pub fn handleExpansion(dev: *@This(), cpu: *Cpu, operation: u16) !void { 154 | switch (cpu.mem[operation]) { 155 | 0x00 => { 156 | // fill [ operation:u8 | len:u16 | srcpg:u16 | src:u16 | value ] 157 | const len = cpu.loadMem(u16, operation + 1); 158 | const page = cpu.loadMem(u16, operation + 3); 159 | const offset = cpu.loadMem(u16, operation + 5); 160 | const value = cpu.loadMem(u8, operation + 7); 161 | 162 | logger.debug("Expansion: Request fill off #{x} bytes (#{x:0>2}) from {x:0>4}:{x:0>4} to {x:0>4}:{x:0>4}", .{ 163 | len, 164 | value, 165 | page, 166 | offset, 167 | page, 168 | offset + len, 169 | }); 170 | 171 | const dst = dev.getPagedSlice(cpu, page, offset, len) orelse { 172 | logger.warn("Expansion: Invalid source page {x:0>4}:{x:0>4}", .{ page, offset }); 173 | 174 | return error.BadExpansion; 175 | }; 176 | 177 | @memset(dst, value); 178 | }, 179 | 180 | 0x01, 0x02 => { 181 | // cpyl, copyr [ operation:u8 | len:u16 | srcpg:u16 | src:u16 | dstpg:u16 | dst:u16] 182 | 183 | const len = cpu.loadMem(u16, operation + 1); 184 | 185 | const src_page = cpu.loadMem(u16, operation + 3); 186 | const src_offset = cpu.loadMem(u16, operation + 5); 187 | 188 | const dst_page = cpu.loadMem(u16, operation + 7); 189 | const dst_offset = cpu.loadMem(u16, operation + 9); 190 | 191 | logger.debug("Expansion: Request move of #{x} bytes from {x:0>4}:{x:0>4} to {x:0>4}:{x:0>4}", .{ 192 | len, 193 | src_page, 194 | src_offset, 195 | dst_page, 196 | dst_offset, 197 | }); 198 | 199 | const src = dev.getPagedSlice(cpu, src_page, src_offset, len) orelse { 200 | logger.warn("Expansion: Invalid source page {x:0>4}:{x:0>4}", .{ src_page, src_offset }); 201 | 202 | return error.BadExpansion; 203 | }; 204 | 205 | const dst = dev.getPagedSlice(cpu, dst_page, dst_offset, len) orelse { 206 | logger.warn("Expansion: Invalid destination page {x:0>4}:{x:0>4}", .{ dst_page, dst_offset }); 207 | 208 | return error.BadExpansion; 209 | }; 210 | 211 | // N.B. this is impossible because all pages are equally sized 212 | // for now, but who knows what the future holds. 213 | if (src.len != dst.len) { 214 | logger.warn("Expansion: Source and destination lengths do not match due to " ++ 215 | "page boundary: {x:0>4}:{x:0>4} -> {x:0>4}:{x:0>4} ({} -> {})", .{ 216 | src_page, src_offset, 217 | dst_page, dst_offset, 218 | src.len, dst.len, 219 | }); 220 | 221 | return error.BadExpansion; 222 | } 223 | 224 | if (cpu.mem[operation] == 0x01) { 225 | // Copy left to right 226 | for (dst[0..len], src) |*d, s| { 227 | d.* = s; 228 | } 229 | } else { 230 | // Copy right to left 231 | var i = src.len; 232 | 233 | while (i > 0) { 234 | i -= 1; 235 | dst[i] = src[i]; 236 | } 237 | } 238 | }, 239 | 240 | // Let's use >0x80 for our own things until the reference implementation assigns them values 241 | 0x80 => { 242 | // Retrieve environment variable 243 | 244 | // [ operation:u8 | name:u16 | dest:u16 | len:u16] 245 | // Retrieve the environment variable with the 0-terminated name referenced by "name" and store 246 | // its value (if any) into the memory pointed to by "dest" (of max. length "len") 247 | const name_ptr = cpu.loadMem(u16, operation + 1); 248 | 249 | const dest_ptr = cpu.loadMem(u16, operation + 3); 250 | const dest_len = cpu.loadMem(u16, operation + 5); 251 | 252 | const env_name = std.mem.sliceTo(cpu.mem[name_ptr..], 0); 253 | var dest = cpu.mem[dest_ptr .. dest_ptr + dest_len]; 254 | 255 | logger.debug("Expansion: Fetch environment variable \"{s}\" (dest len = {})", .{ env_name, dest_len }); 256 | 257 | const env = std.posix.getenv(env_name) orelse ""; 258 | const cpy_len = @min(env.len, dest.len); 259 | 260 | if (cpy_len > 0) { 261 | @memcpy(dest[0..cpy_len], env[0..cpy_len]); 262 | 263 | if (dest.len > env.len) 264 | dest[cpy_len] = 0x00 265 | else 266 | dest[cpy_len - 1] = 0x00; 267 | } 268 | }, 269 | 270 | else => {}, 271 | } 272 | } 273 | }; 274 | -------------------------------------------------------------------------------- /src/lib/varvara/lib.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fs = std.fs; 3 | const mem = std.mem; 4 | 5 | const uxn = @import("uxn-core"); 6 | 7 | const logger = std.log.scoped(.uxn_varvara); 8 | 9 | pub const system = @import("devices/system.zig"); 10 | pub const console = @import("devices/console.zig"); 11 | pub const screen = @import("devices/screen.zig"); 12 | pub const audio = @import("devices/audio.zig"); 13 | pub const controller = @import("devices/controller.zig"); 14 | pub const mouse = @import("devices/mouse.zig"); 15 | pub const file = @import("devices/file.zig"); 16 | pub const datetime = @import("devices/datetime.zig"); 17 | 18 | pub const pages = 4; 19 | 20 | pub fn VarvaraSystem(comptime StdoutWriter: type, comptime StderrWriter: type) type { 21 | return struct { 22 | stderr: StderrWriter, 23 | stdout: StdoutWriter, 24 | 25 | allocator: std.mem.Allocator, 26 | page_table: ?[][uxn.Cpu.page_size]u8 = null, 27 | sandbox_base: ?fs.Dir = null, 28 | 29 | system_device: system.System, 30 | console_device: console.Console, 31 | screen_device: screen.Screen, 32 | audio_devices: [4]audio.Audio, 33 | controller_device: controller.Controller, 34 | mouse_device: mouse.Mouse, 35 | file_devices: [2]file.File, 36 | datetime_device: datetime.Datetime, 37 | 38 | pub fn init( 39 | allocator: std.mem.Allocator, 40 | stdout: StdoutWriter, 41 | stderr: StderrWriter, 42 | ) !@This() { 43 | const page_table = try allocator.alloc([uxn.Cpu.page_size]u8, pages); 44 | 45 | var sys: @This() = .{ 46 | .stderr = stdout, 47 | .stdout = stderr, 48 | 49 | .allocator = allocator, 50 | .page_table = page_table, 51 | 52 | .system_device = .{ .addr = 0x0, .additional_pages = page_table }, 53 | .console_device = .{ .addr = 0x1 }, 54 | .screen_device = .{ .addr = 0x2, .alloc = allocator }, 55 | .audio_devices = .{ 56 | .{ .addr = 0x3 }, 57 | .{ .addr = 0x4 }, 58 | .{ .addr = 0x5 }, 59 | .{ .addr = 0x6 }, 60 | }, 61 | .controller_device = .{ .addr = 0x8 }, 62 | .mouse_device = .{ .addr = 0x9 }, 63 | .file_devices = .{ 64 | .{ .addr = 0xa }, 65 | .{ .addr = 0xb }, 66 | }, 67 | .datetime_device = .{ .addr = 0xc }, 68 | }; 69 | 70 | try sys.screen_device.initializeGraphics(); 71 | 72 | return sys; 73 | } 74 | 75 | pub fn deinit(sys: *@This()) void { 76 | sys.screen_device.cleanupGraphics(); 77 | 78 | for (&sys.file_devices) |*f| 79 | f.cleanup(); 80 | 81 | if (sys.system_device.additional_pages) |page_table| 82 | sys.allocator.free(page_table); 83 | } 84 | 85 | fn filterFileAccess(dev: *file.File, data: ?*anyopaque, path: []const u8, mode: file.File.Mode) bool { 86 | _ = dev; 87 | 88 | var buffer_path: [256]u8 = undefined; 89 | var buffer_self: [256]u8 = undefined; 90 | 91 | const ptr: *const @This() = @ptrCast(@alignCast(data)); 92 | 93 | const file_path = ptr.sandbox_base.?.realpath(path, &buffer_path) catch return false; 94 | const self_path = ptr.sandbox_base.?.realpath(".", &buffer_self) catch return false; 95 | 96 | if (!mem.startsWith(u8, file_path, self_path)) { 97 | logger.warn("Preventing out-of-sandbox {s} access to {s}", .{ @tagName(mode), file_path }); 98 | 99 | return false; 100 | } else { 101 | return true; 102 | } 103 | } 104 | 105 | pub fn sandboxFiles(sys: *@This(), base_dir: fs.Dir) bool { 106 | if (!@hasDecl(file.File, "setAccessFilter")) { 107 | return false; 108 | } 109 | 110 | sys.sandbox_base = base_dir; 111 | 112 | for (&sys.file_devices) |*fd| { 113 | fd.setAccessFilter(sys, filterFileAccess); 114 | } 115 | 116 | return true; 117 | } 118 | 119 | pub fn intercept( 120 | sys: *@This(), 121 | cpu: *uxn.Cpu, 122 | addr: u8, 123 | kind: uxn.Cpu.InterceptKind, 124 | ) !void { 125 | const port: u4 = @truncate(addr & 0xf); 126 | 127 | switch (addr >> 4) { 128 | 0x0 => { 129 | try sys.system_device.intercept(cpu, port, kind); 130 | 131 | if (addr & 0xf >= system.ports.red and 132 | addr & 0xf < system.ports.debug) 133 | { 134 | sys.screen_device.forceRedraw(); 135 | } 136 | }, 137 | 0x1 => try sys.console_device.intercept(cpu, port, kind, sys.stdout, sys.stderr), 138 | 0x2 => try sys.screen_device.intercept(cpu, port, kind), 139 | 0x3 => try sys.audio_devices[0].intercept(cpu, port, kind), 140 | 0x4 => try sys.audio_devices[1].intercept(cpu, port, kind), 141 | 0x5 => try sys.audio_devices[2].intercept(cpu, port, kind), 142 | 0x6 => try sys.audio_devices[3].intercept(cpu, port, kind), 143 | 0x8 => try sys.controller_device.intercept(cpu, port, kind), 144 | 0x9 => try sys.mouse_device.intercept(cpu, port, kind), 145 | 0xa => try sys.file_devices[0].intercept(cpu, port, kind), 146 | 0xb => try sys.file_devices[1].intercept(cpu, port, kind), 147 | 0xc => try sys.datetime_device.intercept(cpu, port, kind), 148 | 149 | else => {}, 150 | } 151 | } 152 | }; 153 | } 154 | 155 | pub const headless_intercepts = struct { 156 | pub const output = .{ 0xc038, 0x0300, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xa260, 0xa260, 0x0000, 0x0000, 0x0000, 0x0000 }; 157 | pub const input = .{ 0x0030, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x07ff, 0x0000, 0x0000, 0x0000 }; 158 | }; 159 | 160 | pub const full_intercepts = struct { 161 | pub const output = .{ 0xff38, 0x0300, 0xc028, 0x8000, 0x8000, 0x8000, 0x8000, 0x0000, 0x0000, 0x0000, 0xa260, 0xa260, 0x0000, 0x0000, 0x0000, 0x0000 }; 162 | pub const input = .{ 0x0030, 0x0000, 0x003c, 0x0014, 0x0014, 0x0014, 0x0014, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x07ff, 0x0000, 0x0000, 0x0000 }; 163 | }; 164 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const uxn = @import("uxn-core"); 4 | const varvara = @import("uxn-varvara"); 5 | 6 | const Cpu = uxn.Cpu; 7 | 8 | pub fn main() void {} 9 | -------------------------------------------------------------------------------- /src/shared.zig: -------------------------------------------------------------------------------- 1 | const build_options = @import("build_options"); 2 | 3 | const std = @import("std"); 4 | const clap = @import("clap"); 5 | 6 | const os = std.os; 7 | 8 | const uxn = @import("uxn-core"); 9 | const uxn_asm = @import("uxn-asm"); 10 | 11 | pub const Debug = @import("Debug.zig"); 12 | 13 | pub const parsers = .{ 14 | .FILE = clap.parsers.string, 15 | .ARG = clap.parsers.string, 16 | .INT = clap.parsers.int(u8, 10), 17 | }; 18 | 19 | pub const LoadResult = struct { 20 | alloc: std.mem.Allocator, 21 | 22 | rom: *[uxn.Cpu.page_size]u8, 23 | debug_symbols: ?Debug, 24 | 25 | pub fn deinit(res: *LoadResult) void { 26 | res.alloc.free(res.rom); 27 | 28 | if (res.debug_symbols) |*debug| 29 | debug.unload(); 30 | } 31 | }; 32 | 33 | pub fn handleCommonArgs( 34 | clap_res: anytype, 35 | params: anytype, 36 | ) ?u8 { 37 | const stderr = std.io.getStdErr().writer(); 38 | 39 | if (clap_res.args.help != 0) { 40 | clap.help(stderr, clap.Help, ¶ms, .{}) catch {}; 41 | 42 | return 0; 43 | } 44 | 45 | if (clap_res.positionals.len < 1) { 46 | stderr.print("Usage: {s} ", .{os.argv[0]}) catch {}; 47 | clap.usage(stderr, clap.Help, ¶ms) catch {}; 48 | stderr.print("\n", .{}) catch {}; 49 | 50 | return 0; 51 | } 52 | 53 | return null; 54 | } 55 | 56 | pub fn loadOrAssembleRom( 57 | alloc: std.mem.Allocator, 58 | input_source: []const u8, 59 | debug_source: ?[]const u8, 60 | ) !LoadResult { 61 | const cwd = std.fs.cwd(); 62 | const input_file = try cwd.openFile(input_source, .{}); 63 | 64 | defer input_file.close(); 65 | 66 | if (build_options.enable_jit_assembly and 67 | std.ascii.endsWithIgnoreCase(input_source, ".tal")) 68 | { 69 | var assembler = uxn_asm.Assembler(.{}).init(alloc, cwd); 70 | defer assembler.deinit(); 71 | 72 | var rom_data = try alloc.create([uxn.Cpu.page_size]u8); 73 | var rom_writer = std.io.fixedBufferStream(rom_data); 74 | 75 | @memset(rom_data[0..], 0x00); 76 | 77 | assembler.include_follow = false; 78 | assembler.default_input_filename = input_source; 79 | 80 | assembler.assemble( 81 | input_file.reader(), 82 | rom_writer.writer(), 83 | rom_writer.seekableStream(), 84 | ) catch |err| { 85 | assembler.issueDiagnostic(err, std.io.getStdErr().writer()) catch {}; 86 | 87 | alloc.free(rom_data); 88 | 89 | return error.AssemblyFailed; 90 | }; 91 | 92 | return .{ 93 | .alloc = alloc, 94 | 95 | .rom = rom_data, 96 | 97 | .debug_symbols = if (debug_source) |_| r: { 98 | var fifo = std.fifo.LinearFifo(u8, .Dynamic).init(alloc); 99 | defer fifo.deinit(); 100 | 101 | try assembler.generateSymbols(fifo.writer()); 102 | 103 | break :r try Debug.loadSymbols(alloc, fifo.reader()); 104 | } else null, 105 | }; 106 | } else { 107 | return .{ 108 | .alloc = alloc, 109 | 110 | .rom = try uxn.loadRom(alloc, input_file), 111 | 112 | .debug_symbols = if (debug_source) |debug_symbols| r: { 113 | const symbols_file = try cwd.openFile(debug_symbols, .{}); 114 | defer symbols_file.close(); 115 | 116 | break :r try Debug.loadSymbols(alloc, symbols_file.reader()); 117 | } else null, 118 | }; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/uxn-asm/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const io = std.io; 3 | 4 | const uxn = @import("uxn-core"); 5 | const uxn_asm = @import("uxn-asm"); 6 | 7 | const clap = @import("clap"); 8 | 9 | const Assembler = uxn_asm.Assembler(.{}); 10 | 11 | fn changeExtension(file: []const u8, ext: []const u8) [256:0]u8 { 12 | var out: [256:0]u8 = [1:0]u8{0x00} ** 256; 13 | 14 | const len = std.mem.lastIndexOfScalar(u8, file, '.') orelse file.len; 15 | 16 | @memcpy(out[0..len], file[0..len]); 17 | @memcpy(out[len .. len + ext.len], ext); 18 | 19 | return out; 20 | } 21 | 22 | pub fn main() !void { 23 | const params = comptime clap.parseParamsComptime( 24 | \\-h, --help Display this help and exit. 25 | \\-s, --symbols Generate symbol file 26 | \\-o, --output Input ROM file name (default: based on input file) 27 | \\ Input source file name 28 | \\ 29 | ); 30 | 31 | var diag = clap.Diagnostic{}; 32 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 33 | 34 | const stderr = std.io.getStdErr().writer(); 35 | const alloc = gpa.allocator(); 36 | 37 | const parsers = comptime .{ 38 | .FILE = clap.parsers.string, 39 | }; 40 | 41 | var res = clap.parse(clap.Help, ¶ms, parsers, clap.ParseOptions{ 42 | .diagnostic = &diag, 43 | .allocator = alloc, 44 | }) catch |err| { 45 | // Report useful error and exit 46 | diag.report(stderr, err) catch {}; 47 | 48 | return err; 49 | }; 50 | 51 | defer res.deinit(); 52 | 53 | if (res.args.help != 0) 54 | return clap.help(stderr, clap.Help, ¶ms, .{}); 55 | 56 | // Argparse end 57 | 58 | var output_rom: [0x10000]u8 = [1]u8{0x00} ** 0x10000; 59 | var output = std.io.fixedBufferStream(&output_rom); 60 | 61 | const input_file_name = res.positionals[0].?; 62 | const input_file = try std.fs.cwd().openFile(input_file_name, .{}); 63 | defer input_file.close(); 64 | 65 | var assembler = Assembler.init(alloc, std.fs.cwd()); 66 | defer assembler.deinit(); 67 | 68 | assembler.include_follow = false; 69 | assembler.default_input_filename = input_file_name; 70 | 71 | assembler.assemble( 72 | input_file.reader(), 73 | output.writer(), 74 | output.seekableStream(), 75 | ) catch |err| { 76 | assembler.issueDiagnostic(err, io.getStdErr().writer()) catch {}; 77 | 78 | return; 79 | }; 80 | 81 | const outfile_name = res.args.output orelse 82 | std.mem.sliceTo(&changeExtension(input_file_name, ".rom"), 0); 83 | 84 | const outfile = try std.fs.cwd().createFile(outfile_name, .{}); 85 | defer outfile.close(); 86 | 87 | try outfile.writer().writeAll(output_rom[0x100..assembler.rom_length]); 88 | 89 | if (res.args.symbols) |symbol_file| { 90 | const symfile = try std.fs.cwd().createFile(symbol_file, .{}); 91 | defer symfile.close(); 92 | 93 | try assembler.generateSymbols(symfile.writer()); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/uxn-cli/main.zig: -------------------------------------------------------------------------------- 1 | const build_options = @import("build_options"); 2 | 3 | const std = @import("std"); 4 | const os = std.os; 5 | const fs = std.fs; 6 | 7 | const clap = @import("clap"); 8 | 9 | const uxn_asm = @import("uxn-asm"); 10 | const uxn = @import("uxn-core"); 11 | const varvara = @import("uxn-varvara"); 12 | const shared = @import("uxn-shared"); 13 | 14 | const Debug = shared.Debug; 15 | 16 | const logger = std.log.scoped(.uxn_cli); 17 | 18 | pub const std_options = std.Options{ 19 | .log_scope_levels = &[_]std.log.ScopeLevel{ 20 | .{ .scope = .uxn_cpu, .level = .info }, 21 | 22 | .{ .scope = .uxn_varvara, .level = .info }, 23 | .{ .scope = .uxn_varvara_system, .level = .info }, 24 | .{ .scope = .uxn_varvara_console, .level = .info }, 25 | .{ .scope = .uxn_varvara_screen, .level = .info }, 26 | .{ .scope = .uxn_varvara_audio, .level = .info }, 27 | .{ .scope = .uxn_varvara_controller, .level = .info }, 28 | .{ .scope = .uxn_varvara_mouse, .level = .info }, 29 | .{ .scope = .uxn_varvara_file, .level = .info }, 30 | .{ .scope = .uxn_varvara_datetime, .level = .info }, 31 | }, 32 | }; 33 | 34 | const VarvaraDefault = varvara.VarvaraSystem(std.fs.File.Writer, std.fs.File.Writer); 35 | 36 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 37 | 38 | fn intercept( 39 | cpu: *uxn.Cpu, 40 | addr: u8, 41 | kind: uxn.Cpu.InterceptKind, 42 | data: ?*anyopaque, 43 | ) !void { 44 | const varvara_sys: ?*VarvaraDefault = @alignCast(@ptrCast(data)); 45 | 46 | if (varvara_sys) |sys| 47 | try sys.intercept(cpu, addr, kind); 48 | } 49 | 50 | pub fn main() !u8 { 51 | const alloc = gpa.allocator(); 52 | 53 | const stdin = std.io.getStdIn().reader(); 54 | const stdout = std.io.getStdOut().writer(); 55 | const stderr = std.io.getStdErr().writer(); 56 | 57 | const params = comptime clap.parseParamsComptime( 58 | \\-h, --help Display this help and exit. 59 | \\ 60 | ++ (if (build_options.enable_jit_assembly) 61 | \\-S, --symbols Load debug symbols (argument ignored if self-assembling) 62 | \\ Input ROM or Tal 63 | \\ 64 | else 65 | \\-S, --symbols Load debug symbols 66 | \\ Input ROM 67 | \\ 68 | ) ++ 69 | \\ 70 | \\... Command line arguments for the module 71 | ); 72 | 73 | var diag = clap.Diagnostic{}; 74 | 75 | const clap_args = clap.ParseOptions{ 76 | .diagnostic = &diag, 77 | .allocator = alloc, 78 | }; 79 | 80 | const res = clap.parse(clap.Help, ¶ms, shared.parsers, clap_args) catch |err| { 81 | // Report useful error and exit 82 | diag.report(stderr, err) catch {}; 83 | 84 | return err; 85 | }; 86 | 87 | defer res.deinit(); 88 | 89 | if (shared.handleCommonArgs(res, params)) |exit| { 90 | return exit; 91 | } 92 | 93 | var env = try shared.loadOrAssembleRom( 94 | alloc, 95 | res.positionals[0].?, 96 | res.args.symbols, 97 | ); 98 | 99 | defer env.deinit(); 100 | 101 | var system = try VarvaraDefault.init(gpa.allocator(), stdout, stderr); 102 | defer system.deinit(); 103 | 104 | if (!system.sandboxFiles(fs.cwd())) { 105 | logger.debug("File implementation does not support sandboxing", .{}); 106 | } 107 | 108 | if (env.debug_symbols) |*d| { 109 | system.system_device.debug_callback = &Debug.onDebugHook; 110 | system.system_device.callback_data = d; 111 | } 112 | 113 | // Setup CPU and intercepts 114 | var cpu = uxn.Cpu.init(env.rom); 115 | 116 | cpu.device_intercept = &intercept; 117 | cpu.callback_data = &system; 118 | 119 | cpu.output_intercepts = varvara.headless_intercepts.output; 120 | cpu.input_intercepts = varvara.headless_intercepts.input; 121 | 122 | // Run initialization vector and push arguments 123 | const args: [][]const u8 = @constCast(res.positionals[1]); 124 | 125 | system.console_device.setArgc(&cpu, args); 126 | 127 | cpu.evaluateVector(0x0100) catch |fault| 128 | try system.system_device.handleFault(&cpu, fault); 129 | 130 | system.console_device.pushArguments(&cpu, args) catch |fault| 131 | try system.system_device.handleFault(&cpu, fault); 132 | 133 | if (system.system_device.exit_code) |c| 134 | return c; 135 | 136 | // Loop until either exit is requested or EOF reached 137 | while (stdin.readByte() catch null) |b| { 138 | system.console_device.pushStdinByte(&cpu, b) catch |fault| 139 | try system.system_device.handleFault(&cpu, fault); 140 | 141 | if (system.system_device.exit_code) |c| 142 | return c; 143 | } 144 | 145 | return 0; 146 | } 147 | -------------------------------------------------------------------------------- /src/uxn-sdl/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fs = std.fs; 3 | 4 | const clap = @import("clap"); 5 | 6 | const uxn = @import("uxn-core"); 7 | const varvara = @import("uxn-varvara"); 8 | const shared = @import("uxn-shared"); 9 | 10 | const Debug = shared.Debug; 11 | 12 | const logger = std.log.scoped(.uxn_sdl); 13 | 14 | pub const SDL = @cImport({ 15 | @cInclude("SDL2/SDL.h"); 16 | }); 17 | 18 | pub const std_options = std.Options{ 19 | .log_scope_levels = &[_]std.log.ScopeLevel{ 20 | .{ .scope = .uxn_cpu, .level = .info }, 21 | 22 | .{ .scope = .uxn_varvara, .level = .info }, 23 | .{ .scope = .uxn_varvara_system, .level = .info }, 24 | .{ .scope = .uxn_varvara_console, .level = .info }, 25 | .{ .scope = .uxn_varvara_screen, .level = .info }, 26 | .{ .scope = .uxn_varvara_audio, .level = .info }, 27 | .{ .scope = .uxn_varvara_controller, .level = .info }, 28 | .{ .scope = .uxn_varvara_mouse, .level = .info }, 29 | .{ .scope = .uxn_varvara_file, .level = .info }, 30 | .{ .scope = .uxn_varvara_datetime, .level = .info }, 31 | }, 32 | }; 33 | 34 | const VarvaraDefault = varvara.VarvaraSystem(std.fs.File.Writer, std.fs.File.Writer); 35 | 36 | var AUDIO_FINISHED: u32 = undefined; 37 | var STDIN_RECEIVED: u32 = undefined; 38 | 39 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 40 | var audio_id: SDL.SDL_AudioDeviceID = undefined; 41 | 42 | fn sdlPanic() noreturn { 43 | const str = @as(?[*:0]const u8, SDL.SDL_GetError()) orelse "unknown error"; 44 | 45 | @panic(std.mem.sliceTo(str, 0)); 46 | } 47 | 48 | fn Callbacks(comptime SystemType: type) type { 49 | return struct { 50 | pub fn intercept( 51 | cpu: *uxn.Cpu, 52 | addr: u8, 53 | kind: uxn.Cpu.InterceptKind, 54 | data: ?*anyopaque, 55 | ) !void { 56 | const varvara_sys: ?*SystemType = @alignCast(@ptrCast(data)); 57 | 58 | if (varvara_sys) |sys| { 59 | const lock_audio = kind == .output and addr >= 0x30 and addr < 0x70; 60 | 61 | if (lock_audio) SDL.SDL_LockAudioDevice(audio_id); 62 | defer if (lock_audio) SDL.SDL_UnlockAudioDevice(audio_id); 63 | 64 | try sys.intercept(cpu, addr, kind); 65 | } 66 | } 67 | 68 | pub fn audioCallback(u: ?*anyopaque, stream: [*c]u8, len: c_int) callconv(.C) void { 69 | const cpu, const sys = @as( 70 | *struct { *uxn.Cpu, *SystemType }, 71 | @alignCast(@ptrCast(u)), 72 | ).*; 73 | 74 | var samples_ptr = @as([*c]i16, @alignCast(@ptrCast(stream))); 75 | var samples = samples_ptr[0 .. @as(usize, @intCast(len)) / 2]; 76 | 77 | // TODO: 0x00 should ideally be SDL_AudioSpec.silence here 78 | @memset(samples, 0x0000); 79 | 80 | for (&sys.audio_devices) |*poly| { 81 | if (poly.duration <= 0) { 82 | poly.evaluateFinishVector(cpu) catch unreachable; 83 | } 84 | 85 | poly.updateDuration(); 86 | poly.renderAudio(samples); 87 | } 88 | 89 | for (0..samples.len) |i| { 90 | samples[i] <<= 6; 91 | } 92 | 93 | //if (still_playing == 0) { 94 | // const audio_id: *SDL.SDL_AudioDeviceID = @alignCast(@ptrCast(u.?)); 95 | // 96 | // SDL.SDL_PauseAudioDevice(audio_id.*, 1); 97 | //} 98 | } 99 | 100 | pub fn receiveStdin(p: ?*anyopaque) callconv(.C) c_int { 101 | const sys: *SystemType = @alignCast(@ptrCast(p)); 102 | 103 | const stdin = std.io.getStdIn().reader(); 104 | 105 | var event: SDL.SDL_Event = .{ .type = STDIN_RECEIVED }; 106 | 107 | while (sys.system_device.exit_code == null) { 108 | const b = stdin.readByte() catch 109 | break; 110 | 111 | event.cbutton.button = b; 112 | 113 | _ = SDL.SDL_PushEvent(&event); 114 | } 115 | 116 | return 0; 117 | } 118 | }; 119 | } 120 | 121 | const InputType = union(enum) { 122 | buttons: varvara.controller.ButtonFlags, 123 | key: u8, 124 | }; 125 | 126 | fn determineInput(event: *SDL.SDL_Event) ?InputType { 127 | const mods = SDL.SDL_GetModState(); 128 | const sym = event.key.keysym.sym; 129 | 130 | if (sym < 0x20 or sym == SDL.SDLK_DELETE) { 131 | return .{ .key = @intCast(sym) }; 132 | } else if (mods & SDL.KMOD_CTRL > 0) { 133 | if (sym < SDL.SDLK_a) { 134 | return .{ .key = @intCast(sym) }; 135 | } else if (sym <= SDL.SDLK_z) { 136 | return .{ .key = @truncate(@as(u32, @bitCast(sym)) - @as(u32, @bitCast(mods & SDL.KMOD_SHIFT)) * 0x20) }; 137 | } 138 | } 139 | 140 | switch (event.key.keysym.sym) { 141 | SDL.SDLK_LCTRL => return .{ .buttons = .{ .ctrl = true } }, 142 | SDL.SDLK_LALT => return .{ .buttons = .{ .alt = true } }, 143 | SDL.SDLK_LSHIFT => return .{ .buttons = .{ .shift = true } }, 144 | SDL.SDLK_HOME => return .{ .buttons = .{ .start = true } }, 145 | SDL.SDLK_UP => return .{ .buttons = .{ .up = true } }, 146 | SDL.SDLK_DOWN => return .{ .buttons = .{ .down = true } }, 147 | SDL.SDLK_LEFT => return .{ .buttons = .{ .left = true } }, 148 | SDL.SDLK_RIGHT => return .{ .buttons = .{ .right = true } }, 149 | 150 | else => {}, 151 | } 152 | 153 | return null; 154 | } 155 | 156 | fn initWindow( 157 | window: **SDL.SDL_Window, 158 | renderer: **SDL.SDL_Renderer, 159 | texture: **SDL.SDL_Texture, 160 | width: u16, 161 | height: u16, 162 | scale: u8, 163 | ) !void { 164 | window.* = SDL.SDL_CreateWindow( 165 | "zuxn", 166 | SDL.SDL_WINDOWPOS_CENTERED, 167 | SDL.SDL_WINDOWPOS_CENTERED, 168 | width * scale, 169 | height * scale, 170 | SDL.SDL_WINDOW_SHOWN, 171 | ) orelse return error.CouldNotCreateWindow; 172 | 173 | errdefer { 174 | SDL.SDL_DestroyWindow(window.*); 175 | } 176 | 177 | renderer.* = SDL.SDL_CreateRenderer( 178 | window.*, 179 | -1, 180 | SDL.SDL_RENDERER_ACCELERATED | SDL.SDL_RENDERER_PRESENTVSYNC, 181 | ) orelse return error.CouldNotCreateRenderer; 182 | 183 | _ = SDL.SDL_RenderSetLogicalSize(renderer.*, width, height); 184 | 185 | errdefer { 186 | SDL.SDL_DestroyRenderer(renderer.*); 187 | } 188 | 189 | texture.* = SDL.SDL_CreateTexture( 190 | renderer.*, 191 | SDL.SDL_PIXELFORMAT_RGB888, 192 | SDL.SDL_TEXTUREACCESS_STREAMING, 193 | width, 194 | height, 195 | ) orelse return error.CouldNotCreateTexture; 196 | } 197 | 198 | fn resizeWindow( 199 | window: *SDL.SDL_Window, 200 | renderer: *SDL.SDL_Renderer, 201 | texture: **SDL.SDL_Texture, 202 | width: u16, 203 | height: u16, 204 | scale: u8, 205 | ) !void { 206 | SDL.SDL_SetWindowSize( 207 | window, 208 | width * scale, 209 | height * scale, 210 | ); 211 | 212 | _ = SDL.SDL_RenderSetLogicalSize(renderer, width, height); 213 | 214 | SDL.SDL_DestroyTexture(texture.*); 215 | 216 | texture.* = SDL.SDL_CreateTexture( 217 | renderer, 218 | SDL.SDL_PIXELFORMAT_RGB888, 219 | SDL.SDL_TEXTUREACCESS_STREAMING, 220 | width, 221 | height, 222 | ) orelse return error.CouldNotCreateTexture; 223 | } 224 | 225 | fn drawScreen( 226 | screen_device: *varvara.screen.Screen, 227 | system_device: *const varvara.system.System, 228 | texture: *SDL.SDL_Texture, 229 | renderer: *SDL.SDL_Renderer, 230 | ) void { 231 | if (screen_device.dirty_region) |region| { 232 | var pixels: [*c]u8 = undefined; 233 | var pitch: c_int = undefined; 234 | 235 | if (SDL.SDL_LockTexture(texture, null, @ptrCast(&pixels), &pitch) != 0) 236 | return; 237 | 238 | defer SDL.SDL_UnlockTexture(texture); 239 | 240 | for (region.y0..region.y1) |y| { 241 | for (region.x0..region.x1) |x| { 242 | const idx = y * screen_device.width + x; 243 | const pal = (@as(u4, screen_device.foreground[idx]) << 2) | screen_device.background[idx]; 244 | 245 | const color = &system_device.colors[if ((pal >> 2) > 0) (pal >> 2) else (pal & 0x3)]; 246 | 247 | pixels[idx * 4 + 3] = 0x00; 248 | pixels[idx * 4 + 2] = color.r; 249 | pixels[idx * 4 + 1] = color.g; 250 | pixels[idx * 4 + 0] = color.b; 251 | } 252 | } 253 | 254 | screen_device.dirty_region = null; 255 | } 256 | 257 | _ = SDL.SDL_RenderCopy(renderer, texture, null, null); 258 | SDL.SDL_RenderPresent(renderer); 259 | } 260 | 261 | fn mainGraphical( 262 | cpu: *uxn.Cpu, 263 | system: *VarvaraDefault, 264 | scale: u8, 265 | args: [][]const u8, 266 | ) !u8 { 267 | if (SDL.SDL_Init(SDL.SDL_INIT_JOYSTICK | SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO) < 0) 268 | sdlPanic(); 269 | 270 | defer SDL.SDL_Quit(); 271 | 272 | _ = SDL.SDL_ShowCursor(SDL.SDL_DISABLE); 273 | 274 | var callback_data = .{ cpu, system }; 275 | 276 | var audio_spec: SDL.SDL_AudioSpec = .{ 277 | .freq = varvara.audio.sample_rate, 278 | .format = SDL.AUDIO_S16SYS, 279 | .channels = 2, 280 | .callback = &Callbacks(VarvaraDefault).audioCallback, 281 | .samples = varvara.audio.sample_count, 282 | .userdata = &callback_data, 283 | 284 | .silence = 0, 285 | .size = 0, 286 | .padding = undefined, 287 | }; 288 | 289 | audio_id = SDL.SDL_OpenAudioDevice(null, 0, &audio_spec, null, 0); 290 | 291 | SDL.SDL_PauseAudioDevice(audio_id, 0); 292 | 293 | STDIN_RECEIVED = SDL.SDL_RegisterEvents(1); 294 | 295 | const stdin = SDL.SDL_CreateThread(&Callbacks(VarvaraDefault).receiveStdin, "stdin", system); 296 | 297 | SDL.SDL_DetachThread(stdin); 298 | 299 | _ = SDL.SDL_JoystickOpen(0) orelse { 300 | logger.debug("Couldn't open joystick {}: {s}", .{ 0, SDL.SDL_GetError() }); 301 | }; 302 | 303 | system.console_device.setArgc(cpu, args); 304 | 305 | var window_width = system.screen_device.width; 306 | var window_height = system.screen_device.height; 307 | 308 | cpu.evaluateVector(0x0100) catch |fault| 309 | try system.system_device.handleFault(cpu, fault); 310 | 311 | system.console_device.pushArguments(cpu, args) catch |fault| 312 | try system.system_device.handleFault(cpu, fault); 313 | 314 | if (system.system_device.exit_code) |c| 315 | return c; 316 | 317 | var window: *SDL.SDL_Window = undefined; 318 | var renderer: *SDL.SDL_Renderer = undefined; 319 | var texture: *SDL.SDL_Texture = undefined; 320 | 321 | // Reset vector is done, all arguments are handled and VM did not exit, 322 | // so we know what our window size should be. 323 | try initWindow( 324 | &window, 325 | &renderer, 326 | &texture, 327 | system.screen_device.width, 328 | system.screen_device.height, 329 | scale, 330 | ); 331 | 332 | main_loop: while (system.system_device.exit_code == null) { 333 | const t0 = SDL.SDL_GetPerformanceCounter(); 334 | 335 | var ev: SDL.SDL_Event = undefined; 336 | 337 | while (SDL.SDL_PollEvent(&ev) != 0) { 338 | switch (ev.type) { 339 | SDL.SDL_QUIT => { 340 | break :main_loop; 341 | }, 342 | 343 | SDL.SDL_MOUSEMOTION => { 344 | system.mouse_device.updatePosition( 345 | cpu, 346 | @truncate(@as(c_uint, @bitCast(ev.motion.x))), 347 | @truncate(@as(c_uint, @bitCast(ev.motion.y))), 348 | ) catch |fault| 349 | try system.system_device.handleFault(cpu, fault); 350 | }, 351 | 352 | SDL.SDL_MOUSEBUTTONDOWN => { 353 | system.mouse_device.pressButtons( 354 | cpu, 355 | @bitCast(@as(u8, 1) << @as(u3, @truncate(ev.button.button - 1))), 356 | ) catch |fault| 357 | try system.system_device.handleFault(cpu, fault); 358 | }, 359 | 360 | SDL.SDL_MOUSEBUTTONUP => { 361 | system.mouse_device.releaseButtons( 362 | cpu, 363 | @bitCast(@as(u8, 1) << @as(u3, @truncate(ev.button.button - 1))), 364 | ) catch |fault| 365 | try system.system_device.handleFault(cpu, fault); 366 | }, 367 | 368 | SDL.SDL_MOUSEWHEEL => { 369 | system.mouse_device.updateScroll(cpu, ev.wheel.x, ev.wheel.y) catch |fault| 370 | try system.system_device.handleFault(cpu, fault); 371 | }, 372 | 373 | SDL.SDL_TEXTINPUT => { 374 | system.controller_device.pressKey(cpu, ev.text.text[0]) catch |fault| 375 | try system.system_device.handleFault(cpu, fault); 376 | }, 377 | 378 | SDL.SDL_KEYDOWN => { 379 | if (determineInput(&ev)) |input| switch (input) { 380 | .buttons => |b| { 381 | system.controller_device.pressButtons(cpu, b, 0) catch |fault| 382 | try system.system_device.handleFault(cpu, fault); 383 | }, 384 | 385 | .key => |k| { 386 | system.controller_device.pressKey(cpu, k) catch |fault| 387 | try system.system_device.handleFault(cpu, fault); 388 | }, 389 | }; 390 | }, 391 | 392 | SDL.SDL_KEYUP => { 393 | if (determineInput(&ev)) |input| switch (input) { 394 | .buttons => |b| { 395 | system.controller_device.releaseButtons(cpu, b, 0) catch |fault| 396 | try system.system_device.handleFault(cpu, fault); 397 | }, 398 | 399 | else => {}, 400 | }; 401 | }, 402 | 403 | SDL.SDL_JOYAXISMOTION => { 404 | const player: u2 = @truncate(@as(c_uint, @bitCast(ev.jbutton.which))); 405 | 406 | _ = player; 407 | 408 | // TODO 409 | }, 410 | 411 | SDL.SDL_JOYBUTTONDOWN, SDL.SDL_JOYBUTTONUP => b: { 412 | const player: u2 = @truncate(@as(c_uint, @bitCast(ev.jbutton.which))); 413 | const btn: varvara.controller.ButtonFlags = switch (ev.jbutton.button) { 414 | 0x0 => .{ .ctrl = true }, 415 | 0x1 => .{ .alt = true }, 416 | 0x06 => .{ .shift = true }, 417 | 0x07 => .{ .start = true }, 418 | else => break :b, 419 | }; 420 | 421 | if (ev.type == SDL.SDL_JOYBUTTONUP) 422 | system.controller_device.releaseButtons(cpu, btn, player) catch |fault| 423 | try system.system_device.handleFault(cpu, fault) 424 | else 425 | system.controller_device.pressButtons(cpu, btn, player) catch |fault| 426 | try system.system_device.handleFault(cpu, fault); 427 | }, 428 | 429 | SDL.SDL_JOYHATMOTION => { 430 | const player: u2 = @truncate(@as(c_uint, @bitCast(ev.jhat.which))); 431 | const btn: varvara.controller.ButtonFlags = switch (ev.jhat.value) { 432 | SDL.SDL_HAT_UP => .{ .up = true }, 433 | SDL.SDL_HAT_DOWN => .{ .down = true }, 434 | SDL.SDL_HAT_LEFT => .{ .left = true }, 435 | SDL.SDL_HAT_RIGHT => .{ .right = true }, 436 | SDL.SDL_HAT_LEFTDOWN => .{ .left = true, .down = true }, 437 | SDL.SDL_HAT_LEFTUP => .{ .left = true, .up = true }, 438 | SDL.SDL_HAT_RIGHTDOWN => .{ .right = true, .down = true }, 439 | SDL.SDL_HAT_RIGHTUP => .{ .right = true, .up = true }, 440 | else => .{}, 441 | }; 442 | 443 | // Release all the non-pressed buttons 444 | const inverse = varvara.controller.ButtonFlags{ 445 | .up = !btn.up, 446 | .down = !btn.down, 447 | .left = !btn.left, 448 | .right = !btn.right, 449 | }; 450 | 451 | system.controller_device.releaseButtons(cpu, inverse, player) catch |fault| 452 | try system.system_device.handleFault(cpu, fault); 453 | 454 | if (@as(u8, @bitCast(btn)) != 0) { 455 | system.controller_device.pressButtons(cpu, btn, player) catch |fault| 456 | try system.system_device.handleFault(cpu, fault); 457 | } 458 | }, 459 | 460 | else => { 461 | if (ev.type == STDIN_RECEIVED) { 462 | system.console_device.pushStdinByte(cpu, ev.cbutton.button) catch |fault| 463 | try system.system_device.handleFault(cpu, fault); 464 | } 465 | }, 466 | } 467 | } 468 | 469 | const t1 = SDL.SDL_GetPerformanceCounter(); 470 | const frametime = @as(f32, @floatFromInt(t1 - t0)) / @as(f32, @floatFromInt(SDL.SDL_GetPerformanceFrequency())) * 1000.0; 471 | 472 | _ = frametime; 473 | 474 | system.screen_device.evaluateFrame(cpu) catch |fault| 475 | try system.system_device.handleFault(cpu, fault); 476 | 477 | if (system.screen_device.width != window_width or system.screen_device.height != window_height) { 478 | window_height = system.screen_device.height; 479 | window_width = system.screen_device.width; 480 | 481 | try resizeWindow( 482 | window, 483 | renderer, 484 | &texture, 485 | system.screen_device.width, 486 | system.screen_device.height, 487 | scale, 488 | ); 489 | } 490 | 491 | drawScreen(&system.screen_device, &system.system_device, texture, renderer); 492 | } 493 | 494 | if (system.system_device.exit_code == null) { 495 | system.system_device.exit_code = 0; 496 | } 497 | 498 | return system.system_device.exit_code.?; 499 | } 500 | 501 | pub fn main() !u8 { 502 | const alloc = gpa.allocator(); 503 | 504 | const params = comptime clap.parseParamsComptime( 505 | \\-h, --help Display this help and exit. 506 | \\-s, --scale Display scale factor 507 | \\-S, --symbols Load debug symbols 508 | \\ Input ROM 509 | \\... Command line arguments for the module 510 | ); 511 | 512 | var diag = clap.Diagnostic{}; 513 | 514 | const stdout = std.io.getStdOut().writer(); 515 | const stderr = std.io.getStdErr().writer(); 516 | 517 | var res = clap.parse(clap.Help, ¶ms, shared.parsers, .{ 518 | .diagnostic = &diag, 519 | .allocator = alloc, 520 | }) catch |err| { 521 | // Report useful error and exit 522 | diag.report(stderr, err) catch {}; 523 | 524 | return err; 525 | }; 526 | 527 | defer res.deinit(); 528 | 529 | if (shared.handleCommonArgs(res, params)) |exit| { 530 | return exit; 531 | } 532 | 533 | var env = try shared.loadOrAssembleRom( 534 | alloc, 535 | res.positionals[0].?, 536 | res.args.symbols, 537 | ); 538 | 539 | defer env.deinit(); 540 | 541 | // Initialize system devices 542 | var system = try VarvaraDefault.init(gpa.allocator(), stdout, stderr); 543 | defer system.deinit(); 544 | 545 | if (!system.sandboxFiles(fs.cwd())) { 546 | logger.debug("File implementation does not suport sandboxing", .{}); 547 | } 548 | 549 | // Setup the breakpoint hook if requested 550 | if (env.debug_symbols) |*d| { 551 | system.system_device.debug_callback = &Debug.onDebugHook; 552 | system.system_device.callback_data = d; 553 | } 554 | 555 | // Setup CPU and intercepts 556 | var cpu = uxn.Cpu.init(env.rom); 557 | 558 | cpu.device_intercept = &Callbacks(VarvaraDefault).intercept; 559 | cpu.callback_data = &system; 560 | 561 | cpu.output_intercepts = varvara.full_intercepts.output; 562 | cpu.input_intercepts = varvara.full_intercepts.input; 563 | 564 | // Run main 565 | return mainGraphical(&cpu, &system, res.args.scale orelse 1, @constCast(res.positionals[1])); 566 | } 567 | --------------------------------------------------------------------------------