diff --git a/.gitignore b/.gitignore index 2317c9e..c0c5a9d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ /build/ /rubies/ +.DS_Store *.wasm *.sav *.sav.tmp @@ -17,6 +18,3 @@ # rspec failure tracking .rspec_status - -# Local Spinel build metadata (Wasm binaries are ignored above) -/docs/spinel/build-info.json diff --git a/BENCHMARK.md b/BENCHMARK.md new file mode 100644 index 0000000..35dbbc9 --- /dev/null +++ b/BENCHMARK.md @@ -0,0 +1,62 @@ +# Benchmarks + +## Choosing a method + +| Runtime | Method | +| --- | --- | +| CRuby (with or without YJIT) | [`rubyboy-bench`](#cruby) | +| Spinel, native executable | [`rubyboy-bench` compiled with Spinel](#spinel) | +| ruby.wasm, Spinel WebAssembly | Not available. The FPS counter on the web page shows drawn frames per second and is not a benchmark. | + +## CRuby + +From the repository root: + +```sh +bundle exec ruby --yjit -Ilib exe/rubyboy-bench \ + --rom-path lib/roms/tobu.gb --frames 1500 --warmup-frames 1500 --count 5 +``` + +| Option | Default | Meaning | +| --- | --- | --- | +| `--rom-path PATH` | `lib/roms/tobu.gb` | ROM to run | +| `--frames N` | 1500 | Measured frames per trial | +| `--warmup-frames N` | 0 | Unmeasured frames before each trial | +| `--count N` | 3 | Number of trials | + +Each trial starts with a fresh emulator and runs the CPU, timer, PPU and APU +without input, saves, drawing, audio output or real-time synchronization. +The command prints each trial's duration and the aggregate FPS (total measured +frames divided by total measured time). + +## Spinel + +Build `rubyboy-bench` with Spinel as described in +[spinel/README.md](spinel/README.md#benchmark), then run it with the same options: + +```sh +build/spinel/rubyboy-bench \ + --rom-path lib/roms/tobu.gb --frames 1500 --warmup-frames 1500 --count 5 +``` + +The executable prints `Ruby: 4.0.7`, which is the Ruby version Spinel reports, +not a CRuby version. Record `spinel --version` instead. + +## Comparing results + +Use the same machine, ROM, `--frames`, `--warmup-frames` and `--count` for every +runtime. Use a warmup so that YJIT has compiled the hot code before measurement. +Record the Ruby version (`ruby -v`) or Spinel revision (`spinel --version`) and the +Rubyboy revision with each result. + +## Results + +Measured on 2026-10-10 with Rubyboy `3484036` on a MacBook Air (Apple M4, macOS 15.5) +on AC power with other applications closed, using the commands above +(`tobu.gb`, `--frames 1500 --warmup-frames 1500 --count 5`). + +| Runtime | Version | FPS | +| --- | --- | ---: | +| CRuby | 4.0.7 | 114 | +| CRuby + YJIT | 4.0.7 | 297 | +| Spinel (native) | `2810a23` | 603 | diff --git a/CHANGELOG.md b/CHANGELOG.md index 130cf83..e878187 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [Unreleased] + +- Keep audio output with --unlimited by dropping audio that cannot be played in time +- Remove the --no-audio player option +- Remove rubyboy-bench --render and --json, and the SDL 2.0.18 requirement +- Stop shipping the rubyboy-wasm build command and the browser files in the gem + ## [1.6.0] - 2026-10-04 - Add battery-backed saves and hardware-oriented save states diff --git a/Gemfile b/Gemfile index f3bb3f4..06fb754 100644 --- a/Gemfile +++ b/Gemfile @@ -10,11 +10,9 @@ gem 'rake', '~> 13.0' group :development, :test do # These bundled gems are required by older RuboCop on recent Ruby versions. gem 'benchmark' - gem 'heap-profiler', '~> 0.7.0' gem 'ostruct' gem 'rspec', '~> 3.12' gem 'rubocop', '~> 1.57' - gem 'stackprof', '~> 0.2.25' end group :wasm do diff --git a/Gemfile.lock b/Gemfile.lock index 5a015ef..074c921 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,7 +3,6 @@ PATH specs: rubyboy (1.6.0) ffi (~> 1.16, >= 1.16.3) - json (~> 2.8) GEM remote: https://rubygems.org/ @@ -13,7 +12,6 @@ GEM benchmark (0.5.0) diff-lcs (1.5.0) ffi (1.16.3) - heap-profiler (0.7.0) js (2.10.1) json (2.18.0) language_server-protocol (3.17.0.3) @@ -64,7 +62,6 @@ GEM logger ruby_wasm (2.10.1-x86_64-linux) logger - stackprof (0.2.25) unicode-display_width (2.5.0) PLATFORMS @@ -75,7 +72,6 @@ PLATFORMS DEPENDENCIES benchmark - heap-profiler (~> 0.7.0) js (= 2.10.1) ostruct rake (~> 13.0) @@ -83,7 +79,6 @@ DEPENDENCIES rubocop (~> 1.57) ruby_wasm (= 2.10.1) rubyboy! - stackprof (~> 0.2.25) BUNDLED WITH 4.0.20 diff --git a/README.md b/README.md index 3af036c..5f9173c 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,9 @@ A Game Boy emulator written in Ruby -**[Try the demo in your browser!](https://sacckey.github.io/rubyboy/)** - Powered by WebAssembly - -The [Spinel version](https://sacckey.github.io/rubyboy/spinel/) uses the same UI -with Ruby compiled to C and then to WebAssembly, without `ruby.wasm`. -See [build instructions](wasm/README.md). +**Try it in your browser:** +[ruby.wasm version](https://sacckey.github.io/rubyboy/) (CRuby compiled to WebAssembly) / +[Spinel version](https://sacckey.github.io/rubyboy/spinel/) (the same Ruby code compiled to C by Spinel, then to WebAssembly) ## Screenshots
@@ -18,125 +16,55 @@ See [build instructions](wasm/README.md).
-## Requirements -[SDL2](https://wiki.libsdl.org/SDL2/Installation) 2.0.18 or higher. - ## Installation -This project requires Ruby 3.2.0 or higher. +Rubyboy requires Ruby 3.2 or higher and [SDL2](https://wiki.libsdl.org/SDL2/Installation). + + $ gem install rubyboy > [!TIP] -> To enhance performance, it is highly recommended to use Ruby 3.3.x with YJIT. +> Run with YJIT (`RUBYOPT=--yjit`) for better performance. -Install the gem and add to the application's Gemfile by executing: +## Usage - $ bundle add rubyboy + $ RUBYOPT=--yjit rubyboy -If bundler is not being used to manage dependencies, install the gem by executing: +### Controls - $ gem install ffi rubyboy +| Key | Button | +| :---: | :---: | +| `W` `A` `S` `D` | ↑ ← ↓ → | +| `K` | A | +| `J` | B | +| `U` | Select | +| `I` | Start | +| `1`–`0` | Save state to slot 1–9, 0 | +| Left `Shift` + `1`–`0` | Load state from that slot | -## Usage +Battery-backed games save to `.sav`, and save states go to `.state`, +next to the ROM file. - $ RUBYOPT=--yjit rubyboy +### Options -| Key | Button | -| :---: | :----: | -| `W` | ↑ | -| `A` | ← | -| `S` | ↓ | -| `D` | → | -| `J` | A | -| `K` | B | -| `U` | Select | -| `I` | Start | - -## Bounded execution - -The player accepts `--frames N` to stop after N completed frames, `--no-audio` -to disable audio output while continuing APU emulation, and `--unlimited` to -disable real-time synchronization: - -```sh -bundle exec ruby --yjit -Ilib exe/rubyboy --frames 600 --unlimited --no-audio lib/roms/tobu.gb -``` - -Normal playback remains synchronized to real time with audio enabled. -`--unlimited` alone still permits audio queue backpressure; use `--no-audio` -for maximum speed. These player options retain keyboard input and battery save -loading/writing. Use the benchmark below for comparisons with a fixed start state. - -## Benchmarking - -Run a headless benchmark with an explicit warmup before each measured trial: - -```sh -bundle exec ruby --yjit -Ilib exe/rubyboy-bench \ - --rom-path lib/roms/tobu.gb --frames 1500 --warmup-frames 1500 --count 5 --json -``` - -Add `--render` to include SDL drawing, with audio output and VSync disabled. -No environment variable is required; the benchmark overrides the VSync setting -for its renderer, including `SDL_RENDER_VSYNC=1`: - -```sh -bundle exec ruby --yjit -Ilib exe/rubyboy-bench \ - --rom-path lib/roms/tobu.gb --frames 1500 --warmup-frames 1500 --count 5 --render --json -``` - -Both modes run the same CPU, timer, PPU and APU frame processing. Each trial -starts with a fresh emulator, ignores battery saves, accepts no game input and -runs without real-time synchronization or audio output. Initialization, warmup -and display cleanup are excluded from the measured duration. Warmup advances -the machine state by the requested number of frames in both modes. The defaults -are 3 trials, 1500 measured frames and no warmup; counts and measured frames must -be positive, and warmup frames may be zero. - -Without `--json`, the command prints each trial's duration and aggregate FPS. -With `--json`, stdout contains one JSON document with individual durations/FPS, -aggregate FPS, runtime identity and execution conditions. Aggregate FPS is the -total measured frame count divided by the sum of measured durations. Closing -the window before a trial finishes aborts the benchmark with a nonzero exit -status instead of reporting a result for that incomplete trial. - -For comparisons, use the same source revision, ROM, frame counts, warmup, -window/display settings and SDL render driver. Normal playback retains its -existing VSync setting. Rendered FPS measures frame -processing and SDL drawing calls; it does not count distinct frames visible on -the monitor. Measure performance without recording, then use a separate -`--render --count 1` run for a reference recording. Preserve elapsed real time -when converting the recording to GIF so that speed differences remain visible. - -## Browser playback (ruby.wasm) - -The existing browser UI uses a worker for emulation and an AudioWorklet for -sound. Use the **Limit to 60 FPS** and **Mute** checkboxes in **Performance** to -change playback while running. Both are checked by default. URL parameters set -their initial state: `?throttle=0&mute=1` for unlimited, muted playback, or -`?throttle=1&mute=0` for normal playback with sound after the first interaction. -The APU continues to run in all modes. - -The large Wasm download remains local-first with the existing proxy fallback; -Wasm files remain excluded from Git. The Ruby VM, Rubyboy sources and ROMs are -packed into one Wasm file. Repack and upload `rubyboy.wasm` after changing the -emulator code. - -```sh -mise exec ruby@4.0.7 -- ruby exe/rubyboy-wasm pack -python3 -m http.server 8765 --bind 127.0.0.1 --directory docs -``` - -See [browser playback and verification](docs/wasm-playback.md) for the build, -verification commands and comparison conditions. +| Option | Effect | +| --- | --- | +| `--frames N` | Exit after N frames | +| `--unlimited` | Run as fast as possible; audio that cannot be played in time is dropped | + +## Benchmark + +On an Apple M4, Rubyboy runs at about 110 FPS on CRuby, 300 FPS with YJIT and +600 FPS when compiled with Spinel. See [BENCHMARK.md](BENCHMARK.md) for the method and details. + +## Other ways to run + +- [WebAssembly builds](wasm/README.md): the browser versions (ruby.wasm and Spinel) +- [Spinel](spinel/README.md): compile Rubyboy to a native executable ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/sacckey/rubyboy. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/sacckey/rubyboy/blob/main/CODE_OF_CONDUCT.md). +Bug reports and pull requests are welcome on GitHub at https://github.com/sacckey/rubyboy. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](CODE_OF_CONDUCT.md). ## License The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). - -## Code of Conduct - -Everyone interacting in the Rubyboy project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/sacckey/rubyboy/blob/main/CODE_OF_CONDUCT.md). diff --git a/docs/emulation-loop.js b/docs/emulation-loop.js index 38450b3..91b204a 100644 --- a/docs/emulation-loop.js +++ b/docs/emulation-loop.js @@ -2,8 +2,6 @@ const CLOCK_HZ = 4_194_304; const FRAME_CYCLES = 70_224; const FRAME_MILLISECONDS = FRAME_CYCLES / CLOCK_HZ * 1000; -const DIRECTION_KEYS = { KeyD: 1, KeyA: 2, KeyW: 4, KeyS: 8 }; -const ACTION_KEYS = { KeyK: 1, KeyJ: 2, KeyU: 4, KeyI: 8 }; export class EmulationLoop { constructor(adapter, emit, options = {}) { @@ -19,7 +17,6 @@ export class EmulationLoop { this.action = 0; this.directionPending = 0; this.actionPending = 0; - this.completedFrames = 0; this.loopHandle = null; this.loopTick = this.tick.bind(this); this.resetClock(); @@ -43,15 +40,6 @@ export class EmulationLoop { this.actionPending |= this.action; } - updateInput(code, pressed) { - const directionMask = DIRECTION_KEYS[code] || 0; - const actionMask = ACTION_KEYS[code] || 0; - this.setInput( - pressed ? this.direction | directionMask : this.direction & ~directionMask, - pressed ? this.action | actionMask : this.action & ~actionMask, - ); - } - releaseInputs() { this.direction = this.action = this.directionPending = this.actionPending = 0; } @@ -64,9 +52,9 @@ export class EmulationLoop { this.directionPending = this.actionPending = 0; } - sendFrame(frameCount) { + sendFrame() { const data = this.adapter.framebuffer(); - this.emit({ type: 'pixelData', data, frameCount, completedFrames: this.completedFrames }, [data]); + this.emit({ type: 'pixelData', data }, [data]); } drainAudio() { @@ -77,12 +65,10 @@ export class EmulationLoop { } runFrame() { - const frames = this.adapter.runFrame(...this.inputMasks()); - this.completedFrames += frames; - // Even an LCD-off fallback advances a frame's CPU time. + this.adapter.runFrame(...this.inputMasks()); this.clearPending(); this.drainAudio(); - this.sendFrame(frames); + this.sendFrame(); } runThrottled() { diff --git a/docs/emulation-worker.js b/docs/emulation-worker.js index f4d493e..70d541a 100644 --- a/docs/emulation-worker.js +++ b/docs/emulation-worker.js @@ -5,17 +5,12 @@ export function startEmulationWorker(initialize) { constructor() { this.adapter = null; this.loop = null; - this.requestId = 0; this.initializing = null; } async init() { - const { adapter, runtime } = await initialize(); - this.adapter = adapter; - this.loop = new EmulationLoop(this.adapter, (message, transfers) => { - postMessage({ ...message, requestId: this.requestId }, transfers); - }); - return runtime; + this.adapter = await initialize(); + this.loop = new EmulationLoop(this.adapter, (message, transfers) => postMessage(message, transfers)); } async ensureInitialized() { @@ -24,12 +19,18 @@ export function startEmulationWorker(initialize) { } async load(data) { + const wasRunning = this.loop.running; this.loop.stop(); - if (data.type === 'loadROM') await this.adapter.loadUploadedRom(data.data); - else await this.adapter.loadPreInstalledRom(data.romName); - this.requestId = data.requestId ?? this.requestId + 1; - this.loop.completedFrames = 0; - postMessage({ type: 'romLoaded', requestId: this.requestId }); + try { + if (data.type === 'loadROM') await this.adapter.loadUploadedRom(data.data); + else await this.adapter.loadPreInstalledRom(data.romName); + } catch (error) { + // A rejected ROM leaves the previous emulator usable. + postMessage({ type: 'error', message: error.message }); + if (wasRunning) this.loop.start(); + return; + } + postMessage({ type: 'romLoaded' }); this.loop.start(); } } @@ -37,17 +38,14 @@ export function startEmulationWorker(initialize) { const rubyboy = new Rubyboy(); const handlers = { async initRubyboy() { - const runtime = await rubyboy.ensureInitialized(); - postMessage({ type: 'initialized', runtime }); + await rubyboy.ensureInitialized(); + postMessage({ type: 'initialized' }); }, startRubyboy() { rubyboy.loop.start(); }, - stopRubyboy() { rubyboy.loop.stop(); }, setThrottle(data) { rubyboy.loop.setThrottle(data.enabled); }, setMute(data) { rubyboy.loop.setMuted(data.enabled); }, input(data) { rubyboy.loop.setInput(data.direction, data.action); }, releaseInputs() { rubyboy.loop.releaseInputs(); }, - keydown(data) { rubyboy.loop.updateInput(data.code, true); }, - keyup(data) { rubyboy.loop.updateInput(data.code, false); }, loadROM(data) { return rubyboy.load(data); }, loadPreInstalledRom(data) { return rubyboy.load(data); }, }; @@ -59,7 +57,7 @@ export function startEmulationWorker(initialize) { if (!handler) return; messages = messages.then(() => handler(data)).catch((error) => { rubyboy.loop?.stop(); - postMessage({ type: 'error', requestId: data.requestId ?? rubyboy.requestId, message: error.message }); + postMessage({ type: 'error', message: error.message }); }); }); } diff --git a/docs/index.js b/docs/index.js index 7d0ea60..0033fef 100644 --- a/docs/index.js +++ b/docs/index.js @@ -126,7 +126,6 @@ function sendInput() { } function releaseInputs() { pressedSources.clear(); - sendInput(); worker.postMessage({ type: 'releaseInputs' }); } for (const type of ['keydown', 'keyup']) { diff --git a/docs/rubyboy-vm.js b/docs/rubyboy-vm.js index 06682e0..88cf9d2 100644 --- a/docs/rubyboy-vm.js +++ b/docs/rubyboy-vm.js @@ -1,29 +1,20 @@ -// CRuby and the Rubyboy sources are packed into the downloaded Wasm. -const ROM_NAMES = new Set(['tobu.gb', 'bgbtest.gb']); - +// Calls the Ruby Executor in either build and moves its output out of the WASI file system. +// Without an executor option, it creates one in the ruby.wasm VM. export class RubyboyVM { constructor(vm, root, File, { executor, toValue } = {}) { - this.vm = vm; this.root = root; this.File = File; - this.toValue = toValue || (value => vm.eval(JSON.stringify(value))); + this.toValue = toValue || (value => vm.wrap(value).call(typeof value === 'number' ? 'to_i' : 'to_s')); this.executor = executor || vm.eval(` require 'js' require '/lib/executor' $executor = Executor.new `); this.inputValues = Array.from({ length: 16 }, (_, value) => this.toValue(value)); - this.fullBudget = this.toValue(32768); } runFrame(direction, action) { - return Number(this.executor.call('exec', this.inputValues[direction], this.inputValues[action]).toString()); - } - - runCycles(cycles, direction, action) { - if (!Number.isInteger(cycles) || cycles <= 0 || cycles > 32768) throw new Error('Invalid cycle budget.'); - const budget = cycles === 32768 ? this.fullBudget : this.toValue(cycles); - return Number(this.executor.call('exec_cycles', budget, this.inputValues[direction], this.inputValues[action]).toString()); + this.executor.call('exec', this.inputValues[direction], this.inputValues[action]); } framebuffer() { @@ -60,7 +51,6 @@ export class RubyboyVM { } loadPreInstalledRom(name) { - if (!ROM_NAMES.has(name)) throw new Error('Unknown bundled ROM.'); this.executor.call('read_pre_installed_rom', this.toValue(name)); this.clearOutputs(); } diff --git a/docs/spinel/rubyboy-spinel.mjs b/docs/spinel/rubyboy-spinel.mjs new file mode 100644 index 0000000..5954c34 --- /dev/null +++ b/docs/spinel/rubyboy-spinel.mjs @@ -0,0 +1,63 @@ +// Run the Spinel-compiled Rubyboy on browser_wasi_shim, with its bundled files in /. +export async function createRubyboySpinel(module, shim) { + const preopen = new shim.PreopenDirectory('/', new Map()); + for (const [path, data] of bundledFiles(module)) put(preopen.dir, path, new shim.File(data, { readonly: true }), shim); + // Spinel opens /dev/null for closed Ruby IO handles. + put(preopen.dir, '/dev/null', new shim.File([]), shim); + const wasi = new shim.WASI([], [], [ + new shim.OpenFile(new shim.File([])), + shim.ConsoleStdout.lineBuffered(line => console.log(line)), + shim.ConsoleStdout.lineBuffered(line => console.warn(line)), + preopen, + ], { debug: false }); + const instance = await WebAssembly.instantiate(module, { wasi_snapshot_preview1: wasi.wasiImport }); + wasi.initialize(instance); + const exports = instance.exports; + + const check = status => { + if (!status) return; + const memory = new Uint8Array(exports.memory.buffer); + const start = exports.rubyboy_error(); + throw new Error(new TextDecoder().decode(memory.subarray(start, memory.indexOf(0, start)))); + }; + const withString = (value, fn) => { + const bytes = new TextEncoder().encode(`${value}\0`); + const pointer = exports.malloc(bytes.length); + new Uint8Array(exports.memory.buffer, pointer, bytes.length).set(bytes); + try { return fn(pointer); } finally { exports.free(pointer); } + }; + withString('/lib/roms/tobu.gb', pointer => check(exports.rubyboy_init(pointer))); + const executor = { + call(method, ...args) { + if (method === 'exec') return check(exports.rubyboy_exec(...args)); + if (method === 'read_rom_from_virtual_fs') return check(exports.rubyboy_read_rom_from_virtual_fs()); + if (method === 'read_pre_installed_rom') return withString(args[0], pointer => check(exports.rubyboy_read_pre_installed_rom(pointer))); + throw new Error(`Unknown method: ${method}`); + }, + }; + return { root: preopen.dir, executor }; +} + +function* bundledFiles(module) { + for (const section of WebAssembly.Module.customSections(module, 'rubyboy-files')) { + const view = new DataView(section); + for (let offset = 0; offset < section.byteLength;) { + const pathLength = view.getUint32(offset, true); + const path = new TextDecoder().decode(new Uint8Array(section, offset + 4, pathLength)); + offset += 4 + pathLength; + const size = view.getUint32(offset, true); + yield [path, new Uint8Array(section, offset + 4, size)]; + offset += 4 + size; + } + } +} + +function put(directory, path, file, shim) { + const parts = path.split('/').filter(Boolean); + const name = parts.pop(); + for (const part of parts) { + if (!directory.contents.has(part)) directory.contents.set(part, new shim.Directory(new Map())); + directory = directory.contents.get(part); + } + directory.contents.set(name, file); +} diff --git a/docs/spinel/spinel-vm.mjs b/docs/spinel/spinel-vm.mjs deleted file mode 100644 index 1bb4134..0000000 --- a/docs/spinel/spinel-vm.mjs +++ /dev/null @@ -1,236 +0,0 @@ -// Host for Spinel's --ext wasm ABI. Supply @bjorn3/browser_wasi_shim as `shim`. -// createSpinelVM(module, {shim, files}) returns {vm, wasi, root}; calls are synchronous. -// Packed spinel.files entries are read-only; files (Map or object) can add writable files. -const encoder = new TextEncoder(); -const decoder = new TextDecoder('utf-8', {ignoreBOM: true}); -const strictDecoder = new TextDecoder('utf-8', {fatal: true, ignoreBOM: true}); -const TYPES = new Set(['int', 'float', 'bool', 'nil', 'string']); - -export class SpinelError extends Error { - constructor(message) { super(message); this.name = 'SpinelError'; } -} - -function pathParts(path, absolute = false) { - if (typeof path !== 'string' || (absolute && !path.startsWith('/')) || path.includes('\0')) { - throw new Error('Invalid virtual filesystem path.'); - } - const parts = path.split('/').filter(Boolean); - if (!parts.length || parts.some(part => part === '.' || part === '..')) { - throw new Error('Invalid virtual filesystem path.'); - } - return parts; -} - -function packedFiles(module) { - const sections = WebAssembly.Module.customSections(module, 'spinel.files'); - if (sections.length > 1) throw new Error('Multiple spinel.files sections.'); - if (!sections.length) return []; - const bytes = new Uint8Array(sections[0]); - const view = new DataView(bytes.buffer); - let offset = 0; - const take = length => { - if (length > bytes.length - offset) throw new Error('Truncated spinel.files section.'); - const value = bytes.subarray(offset, offset + length); - offset += length; - return value; - }; - const u32 = () => { const start = offset; take(4); return view.getUint32(start, true); }; - if (u32() !== 1) throw new Error('Unsupported spinel.files version.'); - const count = u32(); - const entries = []; - const paths = new Set(); - for (let i = 0; i < count; i++) { - const path = strictDecoder.decode(take(u32())); - const canonical = '/' + pathParts(path, true).join('/'); - if (paths.has(canonical)) throw new Error('Duplicate packed file: ' + canonical); - paths.add(canonical); - entries.push([canonical, take(u32())]); - } - if (offset !== bytes.length) throw new Error('Trailing bytes in spinel.files section.'); - return entries; -} - -function mount(root, path, file, Directory) { - const parts = pathParts(path); - let directory = root; - for (const name of parts.slice(0, -1)) { - let child = directory.contents.get(name); - if (!child) { - child = new Directory(new Map()); - child.parent = directory; - directory.contents.set(name, child); - } - if (!(child instanceof Directory)) throw new Error('Virtual filesystem parent is not a directory: ' + path); - directory = child; - } - const name = parts.at(-1); - if (directory.contents.get(name) instanceof Directory) throw new Error('Virtual filesystem path is a directory: ' + path); - directory.contents.set(name, file); -} - -function nullDevice({File, OpenFile, wasi}) { - class OpenNull extends OpenFile { - constructor(file, rights, flags) { - super(file); - // WASI libc can request zero rights; the shim's File permits IO in that case. - this.rights = rights || (BigInt(wasi.RIGHTS_FD_READ) | BigInt(wasi.RIGHTS_FD_WRITE)); - this.flags = flags; - } - fd_fdstat_get() { - const fdstat = new wasi.Fdstat(wasi.FILETYPE_CHARACTER_DEVICE, this.flags); - fdstat.fs_rights_base = this.rights; - return {ret: wasi.ERRNO_SUCCESS, fdstat}; - } - fd_read() { - return {ret: this.rights & BigInt(wasi.RIGHTS_FD_READ) ? wasi.ERRNO_SUCCESS : wasi.ERRNO_BADF, data: new Uint8Array()}; - } - fd_pread() { return this.fd_read(); } - fd_write(data) { - const writable = Boolean(this.rights & BigInt(wasi.RIGHTS_FD_WRITE)); - return {ret: writable ? wasi.ERRNO_SUCCESS : wasi.ERRNO_BADF, nwritten: writable ? data.byteLength : 0}; - } - fd_pwrite(data) { return this.fd_write(data); } - fd_seek(_offset, whence) { - const valid = [wasi.WHENCE_SET, wasi.WHENCE_CUR, wasi.WHENCE_END].includes(whence); - return {ret: valid ? wasi.ERRNO_SUCCESS : wasi.ERRNO_INVAL, offset: 0n}; - } - fd_tell() { return {ret: wasi.ERRNO_SUCCESS, offset: 0n}; } - fd_allocate() { return wasi.ERRNO_SUCCESS; } - fd_filestat_set_size() { return wasi.ERRNO_SUCCESS; } - } - return new class extends File { - constructor() { super(new Uint8Array()); } - get size() { return 0n; } - stat() { return new wasi.Filestat(this.ino, wasi.FILETYPE_CHARACTER_DEVICE, 0n); } - path_open(_oflags, rights, flags) { return {ret: wasi.ERRNO_SUCCESS, fd_obj: new OpenNull(this, rights, flags)}; } - }(); -} - -export class SpinelVM { - constructor(instance) { - this.instance = instance; - this.exports = instance.exports; - const e = this.exports; - if (!(e.memory instanceof WebAssembly.Memory)) throw new Error('Spinel module must export memory.'); - for (const name of ['spinel_init', 'spinel_manifest_ptr', 'spinel_manifest_len', - 'spinel_alloc', 'spinel_free', 'spinel_result_int', 'spinel_result_float', - 'spinel_result_ptr', 'spinel_result_len', 'spinel_result_encoding', - 'spinel_error_ptr', 'spinel_error_len']) { - if (typeof e[name] !== 'function') throw new Error('Missing Spinel ABI export: ' + name); - } - if (e.spinel_init() !== 0) throw this.rubyError(); - const manifest = JSON.parse(strictDecoder.decode(this.bytes(e.spinel_manifest_ptr(), e.spinel_manifest_len()))); - if (manifest.version !== 1 || !Array.isArray(manifest.entries)) throw new Error('Unsupported Spinel manifest.'); - this.entries = new Map(); - for (const entry of manifest.entries) { - if (typeof entry.name !== 'string' || this.entries.has(entry.name) || typeof e[entry.export] !== 'function' - || !Array.isArray(entry.params) || entry.params.some(type => !TYPES.has(type)) - || (!TYPES.has(entry.result) && entry.result !== 'void')) { - throw new Error('Invalid Spinel manifest entry.'); - } - this.entries.set(entry.name, entry); - } - } - - bytes(pointer, length) { - const memory = new Uint8Array(this.exports.memory.buffer); - const start = pointer >>> 0; - if (!Number.isInteger(length) || length < 0 || start > memory.length || length > memory.length - start) { - throw new Error('Invalid Spinel ABI buffer.'); - } - return memory.slice(start, start + length); - } - - rubyError() { - const e = this.exports; - return new SpinelError(decoder.decode(this.bytes(e.spinel_error_ptr(), e.spinel_error_len())) || 'Ruby call failed.'); - } - - call(name, ...args) { return this.invoke(name, args, false); } - - // Copy raw String bytes even for UTF-8 results (including embedded NULs). - callBytes(name, ...args) { return this.invoke(name, args, true); } - - invoke(name, args, rawString) { - const entry = this.entries.get(name); - if (!entry) throw new Error('Unknown Spinel entry: ' + name); - if (args.length !== entry.params.length) throw new TypeError('Wrong argument count for ' + name); - if (rawString && entry.result !== 'string') throw new TypeError('callBytes requires a String result.'); - // Snapshot byte arguments before any native allocation can grow and detach memory. - const inputs = args.map(value => value instanceof Uint8Array ? value.slice() : value); - const allocations = []; - const nativeArgs = []; - const e = this.exports; - try { - entry.params.forEach((type, index) => { - const value = inputs[index]; - if (type === 'string') { - if (typeof value !== 'string' && !(value instanceof Uint8Array)) throw new TypeError('Expected String or Uint8Array argument.'); - const data = typeof value === 'string' ? encoder.encode(value) : value; - if (data.length > 2147483647) throw new RangeError('String argument exceeds the Wasm ABI limit.'); - const pointer = e.spinel_alloc(Math.max(1, data.length)) >>> 0; - if (!pointer) throw new Error('Spinel argument allocation failed.'); - allocations.push(pointer); - new Uint8Array(e.memory.buffer).set(data, pointer); - nativeArgs.push(pointer, data.length, typeof value === 'string' ? 0 : 1); - } else if (type === 'int') { - if (!Number.isInteger(value) || value < -2147483648 || value > 2147483647) throw new RangeError('Expected a signed 32-bit Integer argument.'); - nativeArgs.push(value); - } else if (type === 'float') { - if (typeof value !== 'number') throw new TypeError('Expected a Float argument.'); - nativeArgs.push(value); - } else if (type === 'bool') { - if (typeof value !== 'boolean') throw new TypeError('Expected a Boolean argument.'); - nativeArgs.push(Number(value)); - } else { - if (value !== null) throw new TypeError('Expected a null argument.'); - nativeArgs.push(0); - } - }); - if (e[entry.export](...nativeArgs) !== 0) throw this.rubyError(); - if (entry.result === 'string') { - const data = this.bytes(e.spinel_result_ptr(), e.spinel_result_len()); - const encoding = e.spinel_result_encoding(); - if (encoding !== 0 && encoding !== 1) throw new Error('Invalid Spinel String encoding.'); - return rawString || encoding === 1 ? data : decoder.decode(data); - } - if (entry.result === 'int') return e.spinel_result_int(); - if (entry.result === 'float') return e.spinel_result_float(); - if (entry.result === 'bool') return Boolean(e.spinel_result_int()); - if (entry.result === 'nil') return null; - return null; - } finally { - for (const pointer of allocations) e.spinel_free(pointer); - } - } -} - -export async function createSpinelVM(module, options = {}) { - const {WASI, File, Directory, OpenFile, PreopenDirectory, ConsoleStdout} = options.shim || {}; - if ([WASI, File, Directory, OpenFile, PreopenDirectory, ConsoleStdout].some(value => typeof value !== 'function')) { - throw new TypeError('Supply the browser_wasi_shim module as options.shim.'); - } - const preopen = new PreopenDirectory('/', new Map()); - const root = preopen.dir; - for (const [path, data] of packedFiles(module)) mount(root, path, new File(data, {readonly: true}), Directory); - const files = options.files instanceof Map ? options.files : Object.entries(options.files || {}); - for (const [path, data] of files) { - const file = data instanceof File ? data : new File(typeof data === 'string' ? encoder.encode(data) : data); - mount(root, path, file, Directory); - } - // Spinel uses a shared /dev/null descriptor for closed Ruby IO handles. - const dev = root.contents.get('dev'); - if (!(dev instanceof Directory) || !dev.contents.has('null')) { - mount(root, '/dev/null', nullDevice(options.shim), Directory); - } - const env = Array.isArray(options.env) ? options.env : Object.entries(options.env || {}).map(([key, value]) => key + '=' + value); - const wasi = new WASI(options.args || ['spinel'], env, [ - new OpenFile(new File(options.stdin || new Uint8Array())), - ConsoleStdout.lineBuffered(options.stdout || (line => console.log(line))), - ConsoleStdout.lineBuffered(options.stderr || (line => console.error(line))), - preopen, - ], {debug: options.debug ?? false}); - const instance = await WebAssembly.instantiate(module, {wasi_snapshot_preview1: wasi.wasiImport}); - wasi.initialize(instance); - return {vm: new SpinelVM(instance), wasi, root}; -} diff --git a/docs/spinel/worker.js b/docs/spinel/worker.js index 1a42a85..e3a9f00 100644 --- a/docs/spinel/worker.js +++ b/docs/spinel/worker.js @@ -1,5 +1,5 @@ import * as shim from 'https://cdn.jsdelivr.net/npm/@bjorn3/browser_wasi_shim@0.4.2/+esm'; -import { createSpinelVM } from './spinel-vm.mjs'; +import { createRubyboySpinel } from './rubyboy-spinel.mjs'; import { RubyboyVM } from '../rubyboy-vm.js'; import { startEmulationWorker } from '../emulation-worker.js'; @@ -8,11 +8,6 @@ startEmulationWorker(async () => { if (!response.ok) response = await fetch('https://proxy.sacckey.dev/rubyboy-spinel.wasm'); if (!response.ok) throw new Error(`Wasm download failed (${response.status}).`); const module = await WebAssembly.compile(await response.arrayBuffer()); - const { vm, root } = await createSpinelVM(module, { shim }); - vm.call('RubyboyBrowser.init', '/lib/roms/tobu.gb'); - const executor = { call: (method, ...args) => vm.call(`RubyboyBrowser.${method}`, ...args) }; - return { - adapter: new RubyboyVM(vm, root, shim.File, { executor, toValue: value => value }), - runtime: { backend: 'spinel' }, - }; + const { root, executor } = await createRubyboySpinel(module, shim); + return new RubyboyVM(null, root, shim.File, { executor, toValue: value => value }); }); diff --git a/docs/wasm-playback.md b/docs/wasm-playback.md deleted file mode 100644 index 653b20a..0000000 --- a/docs/wasm-playback.md +++ /dev/null @@ -1,267 +0,0 @@ -# Existing ruby.wasm player - -The player keeps the existing console UI, canvas size and keyboard mappings. -The **Performance** section has **Limit to 60 FPS** and **Mute** checkboxes for -changing playback while running. The local `./rubyboy.wasm` download and existing -`https://proxy.sacckey.dev/rubyboy.wasm` fallback remain unchanged. No Wasm binary -needs to be committed to Git. - -Execution and audio use a worker and an AudioWorklet. The scheduler is separate -from the Ruby VM adapter so a future Spinel build can use the same scheduler -and transfer path. - -## Packed Ruby sources - -`rubyboy.wasm` contains the Ruby VM, Rubyboy Ruby sources and bundled ROMs. -The pack command embeds `./lib` at `/lib`, and the browser loads `/lib/executor` -directly from the Wasm. There is no separate source download or JSON mounting. -Repack and upload the Wasm after changing any emulator source; built-in ROMs -remain under `/lib/roms`. - -## Build Ruby 4.0.7 and pack the emulator - -The build pins the stable Ruby 4.0.7 source and verifies its SHA-256 before -extracting it. `ruby_wasm` and `js` are pinned to 2.10.1. The toolkit's `4.0` -alias currently points to Ruby 4.0.0; `exe/rubyboy-wasm` adds a versioned source -configuration so a build cannot silently reuse that older alias. - -Install a host Ruby 4.0.7 and Node 24.4.0, then run from the repository root: - -```sh -mise install ruby@4.0.7 node@24.4.0 -mise exec ruby@4.0.7 -- gem install bundler -v 4.0.20 --no-document -BUNDLE_IGNORE_CONFIG=1 BUNDLE_ONLY=wasm mise exec ruby@4.0.7 -- bundle install -mise exec ruby@4.0.7 -- ruby exe/rubyboy-wasm build -mise exec ruby@4.0.7 -- ruby exe/rubyboy-wasm pack -``` - -The build downloads WASI SDK 24.0 and Binaryen 108 and compiles a native -build-time Ruby and the wasm32-wasi Ruby VM. It needs the platform's native -C/C++ build tools, `make`, `curl`, and `tar`. On macOS these come from Xcode -Command Line Tools (`xcode-select --install`). The first build can take several -minutes. Build products and caches stay in Git-ignored `build/` and `rubies/`. -The output is `docs/ruby-js.wasm` and then `docs/rubyboy.wasm`. By default, -`pack` uses the SDK's `llvm-objcopy --strip-debug` to remove DWARF custom sections. -Executable code, data and function names are retained. For C-level debugging, -use `ruby exe/rubyboy-wasm pack --keep-debug`. Keep both files -out of Git; upload the latter through the existing release/download workflow. -Deploy the updated SDK imports and new Wasm together so the JavaScript SDK and -the embedded `js` extension have matching versions. - -YJIT and ZJIT are unavailable on wasm32-wasi. Enabling YJIT in the host Ruby -only affects build-time Ruby execution; it does not enable JIT inside the Wasm -VM. The target C compiler already uses `-O3 -fno-fast-math`, and the toolkit -applies Binaryen `-O3` during its Asyncify step. Further optimization needs a -measured comparison rather than assuming a higher optimization level is faster. - -## Run - -```sh -python3 -m http.server 8765 --bind 127.0.0.1 --directory docs -``` - -| URL | Execution | Sound | -| --- | --- | --- | -| `http://127.0.0.1:8765/` | Normal speed | Muted | -| `http://127.0.0.1:8765/?throttle=0&mute=1` | Unlimited | Muted | -| `http://127.0.0.1:8765/?throttle=1&mute=0` | Normal speed | Enabled after a key or button interaction | - -Both checkboxes are checked by default; URL parameters override their initial -state. Uncheck **Limit to 60 FPS** for unlimited execution. Uncheck **Mute** to -start sound from that interaction, and check it to clear queued audio and -suspend playback. The current checkbox states are used even if they change -before the Ruby VM finishes loading. - -Both modes advance CPU, timer, PPU and APU. Muting suppresses audio transfer and -playback after generating and consuming the APU samples; it does not skip APU -emulation. `mute=0` also permits audio in unlimited mode, but audio is generated -faster than real time, so that combination is not normal-speed playback. - -Both modes execute and present one frame-step per worker task. Normal mode -paces tasks at `70,224 / 4,194,304` seconds per frame (about 59.73 FPS), including -emulation and buffer processing in that interval. It waits only when the frame -finishes early. If the core is slower, the next task runs without an additional -wait. A delayed timer or slow core does not accumulate a batch of hidden -catch-up frames; game time advances at the available processing speed. -Unlimited mode removes the deadline. LCD-disabled execution remains bounded by -one frame's CPU time so the worker can accept further input. - -Completed 512-frame stereo APU blocks are packed as little-endian Float32. -The worklet holds at most 2,048 stereo frames and resamples 48 kHz input if the -device rate differs. ROM changes clear queued audio and reset the execution -clock. Keyboard mappings stay K→A, J→B, I→Start and U→Select. - -## Verification - -The JavaScript unit tests need Node: - -```sh -node scripts/test_wasm_frontend.cjs -``` - -With the project's development gems installed, the Ruby adapter test and -repository lint check are: - -```sh -ruby -S rspec -Ilib spec/rubyboy/emulator_wasm_spec.rb -ruby -S rubocop -``` - -The actual Ruby VM test also needs Ruby and the same JavaScript dependencies -used by the existing page. Install these under the Git-ignored build directory: - -```sh -npm install --prefix build/browser-runtime --no-audit --no-fund \ - @ruby/wasm-wasi@2.10.1 @bjorn3/browser_wasi_shim@0.4.2 -node scripts/verify_wasm_package.mjs /path/to/rubyboy.wasm -node scripts/test_wasm_runtime.mjs /path/to/rubyboy.wasm -``` - -If the Wasm is at `docs/rubyboy.wasm`, omit the path argument. -`verify_wasm_package.mjs` checks Ruby 4.0.7, the current Rubyboy version, and the -file list, size and SHA-256 of every regular file packed under `/lib`. -`test_wasm_runtime.mjs` prints the host Ruby description, Wasm Ruby description, -Wasm hash and ROM hash. It compares pixels and Float32 audio with CRuby for both -frame execution and split cycle budgets, including input changes. It also -checks source loading, copied buffers, audio consumption, instruction overshoot, -LCD-off behavior and the existing packed ROMs. - -## Check an additional Wasm optimization - -The following is a fixed experiment, not a claim that `-O4` is faster. -Prepare the candidate before running either artifact's benchmark: - -```sh -mkdir -p build/wasm-rebuild -mise exec ruby@4.0.7 -- ruby exe/rubyboy-wasm pack --keep-debug -cp docs/rubyboy.wasm build/wasm-rebuild/rubyboy-ruby4.0.7-baseline.wasm -build/toolchain/binaryen/bin/wasm-opt \ - build/wasm-rebuild/rubyboy-ruby4.0.7-baseline.wasm \ - --strip-dwarf -O4 --converge -g \ - -o build/wasm-rebuild/rubyboy-ruby4.0.7-opt.wasm -mise exec ruby@4.0.7 node@24.4.0 -- node scripts/test_wasm_runtime.mjs \ - build/wasm-rebuild/rubyboy-ruby4.0.7-opt.wasm -mise exec node@24.4.0 -- node scripts/compare_wasm_optimization.mjs \ - build/wasm-rebuild/rubyboy-ruby4.0.7-baseline.wasm \ - build/wasm-rebuild/rubyboy-ruby4.0.7-opt.wasm \ - build/wasm-rebuild/ruby4.0.7-opt-comparison.json -mise exec ruby@4.0.7 -- ruby exe/rubyboy-wasm pack -``` - -Install the JavaScript dependencies from the Verification section first. -The benchmark runs five alternating pairs: baseline/candidate, candidate/baseline, -baseline/candidate, candidate/baseline, baseline/candidate. Every trial starts a -fresh Node process and Ruby VM, runs 300 warmup frame-step calls, then times -600 calls with all buttons released. It validates the same Ruby sources packed under `/lib` and uses the same Tobu ROM, keeps the APU active, and includes framebuffer/audio VFS -copies plus transfers of both buffers. SHA-256 calculation is outside timing. -Frame calls run in a synchronous loop; no scheduler, real-time synchronization, -60 FPS limiter, VSync, or audio-device backpressure is used. - -This is a Node emulation-and-transfer measurement. It excludes Canvas drawing, -AudioWorklet playback, worker task scheduling and browser rendering. It is not -the browser's visible FPS counter or a Spinel comparison. Results cover this -fixed ROM segment and workload; they do not establish performance for all games. -Cold Wasm compilation and VM initialization are recorded separately from warmed -throughput, excluding network transfer and Node startup. - -The selection rule was set before measurement: all ten trials must produce -identical final framebuffer, full measured Float32 stereo audio, and final -emulator state hashes; the candidate must be faster in every paired trial; and -its median paired FPS gain must be at least 5%. Otherwise retain the standard -artifact for throughput. The JSON retains every trial, input/source/artifact -hash, runtime identity and separate cold timings; do not drop slower trials. - -## Measured result: 2026-10-04 - -These historical measurements used the former JSON source-loading path. The -current player and benchmark load packed `/lib` sources directly. The results -below are retained as a record of that earlier experiment; they are not new -measurements of the current startup path. - -Environment: Apple M4 (10 logical CPUs), arm64, Darwin 24.5.0, -Node v24.4.0 / V8 13.6.233.10-node.17; -Ruby `4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [wasm32-wasi]`; -YJIT and ZJIT disabled. Both artifacts use WASI SDK 24.0, compiler -`-O3 -fno-fast-math`, ruby.wasm/js/JavaScript SDK 2.10.1, shim 0.4.2 and -Binaryen 108. The benchmark script and selection conditions were prepared -before measuring either artifact. All compilation and optimizer jobs finished, -previous emulator test tabs were closed, and a fixed 30-second pause preceded -the sequential benchmark. Ordinary desktop activity was not controlled. - -| Pair | Order | Baseline seconds | Candidate seconds | Baseline FPS | Candidate FPS | Paired FPS gain | -| --- | --- | ---: | ---: | ---: | ---: | ---: | -| 1 | baseline → candidate | 9.990348 | 9.929312 | 60.058 | 60.427 | 0.615% | -| 2 | candidate → baseline | 10.006520 | 9.947389 | 59.961 | 60.317 | 0.594% | -| 3 | baseline → candidate | 10.044488 | 9.910858 | 59.734 | 60.540 | 1.348% | -| 4 | candidate → baseline | 9.997161 | 9.889041 | 60.017 | 60.673 | 1.093% | -| 5 | baseline → candidate | 9.974955 | 9.877491 | 60.151 | 60.744 | 0.987% | - -Median throughput: **60.017 → 60.540 FPS**. -The median paired FPS gain was **0.987%**. All five pairs favored -the candidate, but the predeclared 5% adoption condition was not met. Therefore -`-O4 --converge` is **not** added to the standard build. This is a small observed -difference in one workload, not evidence of a general emulator speedup. - -Median first-process Wasm compile time was -14.134 ms → 11.764 ms; -median fresh-VM initialization/mount/ROM load time was -190.534 ms → 299.304 ms. -These are separate measurements and include no network transfer. - -All ten trials agreed on 600 returned frame steps, 3,846,144 measured audio -bytes and final state. SHA-256 values: - -- Framebuffer: `dc09d1947f18ca374ed04ae87beffa88c69a1fe8033bb086cb82af81365bd23d` -- Measured audio: `31c426285e369204dbc790059a7cc32669978ff7c7f0fd927834274638a82589` -- Final emulator Marshal state: `3fa7a02d1cb7aa113bf900ecee12cfb6e0a13bfd79a65e662ec3bf81781065ef` -- Source bundle: `af47e2bb696652526aec936af684c94d76c19d880ffddcdc881c9af1a9000fe3` -- Tobu ROM: `5d3871cae77db2287807e8914fd21f76203e73aa3fec20955489d45cb4571d8f` -- Benchmark script: `8b64c5fde26cf0042052b396461b0a602a856054b507927634683d6bd58bafd5` - -| Artifact | Bytes | SHA-256 | -| --- | ---: | --- | -| Standard build with DWARF (benchmark baseline) | 53,755,170 | `9f4a788d975517a514a7002a6336fe1c0be0abaff2ce95ce7f9bfad1795d12cb` | -| Additional `-O4 --converge` candidate | 32,901,233 | `d312fcb234d85c8da3fab20c1c039eccc9e485091939ac2fa9ada49328cf8a6b` | -| Standard build after `llvm-objcopy --strip-debug` (distribution output) | 33,644,659 | `d5cce4e1ece27c23a34f9add63977ed7f1c8e230e8cf85d1b658a153c7930492` | - -Distribution uses **debug information removal only**, a **37.41%** reduction -from the unstripped standard artifact. This was checked separately after the -throughput experiment: every non-custom Wasm section (including code and data) -remained byte-for-byte identical to the baseline; the `name`, `producers` and -`target_features` sections remain. No additional runtime optimization was -selected, and no FPS gain is claimed for this size reduction. Both the direct -package check and the CRuby pixel/audio comparison also passed for this final -artifact. Browser startup, gameplay input, live throttle/mute changes and -AudioWorklet loading were also checked with no reported browser errors; this -background-tab check was separate from the throughput measurement. Packing -includes exactly 57 regular files: 34 Ruby sources and 17 ROMs. - -All trial records, fixture hashes and cold timings are retained locally at -`build/wasm-rebuild/ruby4.0.7-opt-comparison.json`. Running the commands above -regenerates the full report on another machine. Absolute paths and debug info -can vary between builds, so compare source/ROM hashes and actual runtime identity -rather than expecting an identical Wasm hash across machines. - -## Browser and Spinel comparison conditions - -For a browser or Spinel comparison, use identical Ruby source hashes, ROM bytes, input sequence, -frame counts, warmup, throttle/mute settings, browser/foreground state, Canvas -scaling, scheduler and transfer policy. Report the actual Ruby VM version and -Wasm/compiler identities as well. - -The visible FPS counter retains its existing meaning: received/drawn images -in the last second. Each worker task now presents the frame-step it computes. -The display counter still measures message arrivals rather than a timed core benchmark. `pixelData.frameCount` and -`pixelData.completedFrames` separately report emulated frame counts. Use those -counts and elapsed time for processing throughput; do not infer a compiler -speedup from the visible counter. Recording adds work, so time benchmarks -separately from recordings. - -Both modes count bounded frame-step calls, including an LCD-off CPU-time fallback. -Use the same mode and frame-count definition for both builds, especially with -ROMs that temporarily disable their LCD. - -The Ruby VM adapter packs output through the WASI virtual filesystem. A future -Spinel adapter may expose linear-memory buffers directly. An end-to-end player -comparison includes that bridge cost; a claim about core compilation speed -also needs a separate measurement of the emulation core. diff --git a/docs/worker.js b/docs/worker.js index 87afa74..850628d 100644 --- a/docs/worker.js +++ b/docs/worker.js @@ -7,10 +7,8 @@ startEmulationWorker(async () => { // Keep the existing local-first download and proxy fallback. let response = await fetch('./rubyboy.wasm'); if (!response.ok) response = await fetch('https://proxy.sacckey.dev/rubyboy.wasm'); + if (!response.ok) throw new Error(`Wasm download failed (${response.status}).`); const module = await WebAssembly.compileStreaming(response); const { vm, wasi } = await DefaultRubyVM(module); - return { - adapter: new RubyboyVM(vm, wasi.fds[3].dir, File), - runtime: { ruby: vm.eval('RUBY_DESCRIPTION').toString() }, - }; + return new RubyboyVM(vm, wasi.fds[3].dir, File); }); diff --git a/exe/rubyboy b/exe/rubyboy index 89f0469..52babf1 100755 --- a/exe/rubyboy +++ b/exe/rubyboy @@ -6,7 +6,6 @@ require 'optparse' frames = nil realtime = true -audio = true begin OptionParser.new do |opts| @@ -18,8 +17,7 @@ begin frames = value end - opts.on('--unlimited', 'Disable real-time synchronization') { realtime = false } - opts.on('--no-audio', 'Disable audio output while keeping APU emulation') { audio = false } + opts.on('--unlimited', 'Disable real-time synchronization; excess audio is dropped') { realtime = false } opts.on('-h', '--help', 'Displays this help') do puts opts @@ -41,4 +39,4 @@ else puts 'YJIT is not available in this environment.' end -Rubyboy::Emulator.new(rom_path, audio:).start(frames:, realtime:) +Rubyboy::Emulator.new(rom_path).start(frames:, realtime:) diff --git a/exe/rubyboy-bench b/exe/rubyboy-bench index fecfc96..692381f 100755 --- a/exe/rubyboy-bench +++ b/exe/rubyboy-bench @@ -11,23 +11,15 @@ begin opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} [options]" opts.on('--count COUNT', Integer, 'Number of trials for the benchmark') do |count| - options[:count] = Integer(count) + options[:count] = count end opts.on('--frames FRAMES', Integer, 'Number of measured frames per trial') do |frames| - options[:frames] = Integer(frames) + options[:frames] = frames end opts.on('--warmup-frames FRAMES', Integer, 'Number of unmeasured warmup frames per trial (default: 0)') do |frames| - options[:warmup_frames] = Integer(frames) - end - - opts.on('--render', 'Draw each frame without audio output or realtime synchronization') do - options[:render] = true - end - - opts.on('--json', 'Output benchmark results and runtime information as JSON') do - options[:json] = true + options[:warmup_frames] = frames end opts.on('--rom-path ROM_PATH', String, 'Path to the ROM file') do |rom_path| @@ -45,18 +37,16 @@ rescue OptionParser::ParseError, ArgumentError => e exit 1 end -unless options[:json] - puts "Ruby: #{RUBY_VERSION}" - if defined?(RubyVM::YJIT) - puts "YJIT: #{RubyVM::YJIT.enabled?}" - else - puts 'YJIT is not available in this environment.' - end +puts "Ruby: #{RUBY_VERSION}" +if defined?(RubyVM::YJIT) + puts "YJIT: #{RubyVM::YJIT.enabled?}" +else + puts 'YJIT is not available in this environment.' end begin Rubyboy::Bench.new.run(**options) -rescue ArgumentError, Rubyboy::Bench::WindowClosed => e +rescue ArgumentError => e warn e.message exit 1 end diff --git a/exe/rubyboy-wasm b/exe/rubyboy-wasm deleted file mode 100755 index 924c47e..0000000 --- a/exe/rubyboy-wasm +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -case ARGV -when ['build'] - command = %w[build --ruby-version 4.0.7 -o ./docs/ruby-js.wasm] -when ['pack'], ['pack', '--keep-debug'] - command = %w[pack ./docs/ruby-js.wasm --dir ./lib::/lib -o ./docs/rubyboy.wasm] -else - puts "Invalid argument. Use 'build' or 'pack [--keep-debug]'." - exit 1 -end - -ENV['BUNDLE_ONLY'] = 'wasm' -ENV['BUNDLE_IGNORE_CONFIG'] = '1' - -require 'bundler/setup' -require 'digest' -require 'fileutils' -require 'tempfile' -require 'tmpdir' -require 'ruby_wasm' -require 'ruby_wasm/cli' - -# The toolkit's 4.0 alias still uses Ruby 4.0.0. Build the pinned stable source -# with its 4.0 extensions and SDK, using separate caches for Ruby 4.0.7. -class RubyboyWasmCLI < RubyWasm::CLI - RUBY_SOURCE_VERSION = '4.0.7' - RUBY_SOURCE_URL = 'https://cache.ruby-lang.org/pub/ruby/4.0/ruby-4.0.7.tar.gz' - RUBY_SOURCE_SHA256 = '911ace20f90d068ca0e4dda6d0e4f0f81e52e52f2dd4f4004c721e253412e82d' - - def self.build_config_aliases(root) - aliases = super - aliases[RUBY_SOURCE_VERSION] = aliases.fetch('4.0').merge( - name: RUBY_SOURCE_VERSION, - src: { - name: RUBY_SOURCE_VERSION, - type: 'local', - path: prepare_ruby_source(root), - patches: [] - } - ) - aliases - end - - def self.prepare_ruby_source(root) - cache_dir = File.join(root, 'build', 'sources') - archive = File.join(cache_dir, "ruby-#{RUBY_SOURCE_VERSION}.tar.gz") - source_dir = File.join(cache_dir, "ruby-#{RUBY_SOURCE_VERSION}") - FileUtils.mkdir_p(cache_dir) - - unless File.file?(archive) && Digest::SHA256.file(archive).hexdigest == RUBY_SOURCE_SHA256 - Tempfile.create(['ruby-source-', '.tar.gz'], cache_dir) do |download| - system('curl', '-fL', '--retry', '3', '-o', download.path, RUBY_SOURCE_URL, exception: true) - raise "Ruby #{RUBY_SOURCE_VERSION} source checksum mismatch" unless Digest::SHA256.file(download.path).hexdigest == RUBY_SOURCE_SHA256 - - FileUtils.mv(download.path, archive) - end - end - return source_dir if File.file?(File.join(source_dir, 'configure')) - - staging_dir = Dir.mktmpdir('ruby-source-', cache_dir) - begin - system('tar', '-xzf', archive, '-C', staging_dir, '--strip-components=1', exception: true) - FileUtils.rm_rf(source_dir) - FileUtils.mv(staging_dir, source_dir) - ensure - FileUtils.rm_rf(staging_dir) - end - source_dir - end -end - -# Exclude all gems except the 'js' gem for packaging -definition = Bundler.definition -excluded_gems = definition.resolve.materialize(definition.requested_dependencies).map(&:name) -excluded_gems -= %w[js] -RubyWasm::Packager::EXCLUDED_GEMS.concat(excluded_gems) - -RubyboyWasmCLI.new(stdout: $stdout, stderr: $stderr).run(command) - -# Keep executable sections unchanged while removing unused C debug information. -# The SDK is installed by the build command; --keep-debug retains the full module. -if ARGV == ['pack'] - build_root = File.expand_path(ENV['RUBY_WASM_ROOT'] || Bundler.root.to_s) - objcopy = File.join(build_root, 'build', 'toolchain', 'wasi-sdk-24.0', 'bin', 'llvm-objcopy') - raise "WASI SDK 24.0 is missing; run 'ruby exe/rubyboy-wasm build' first or use 'pack --keep-debug'" unless File.executable?(objcopy) - - wasm_path = './docs/rubyboy.wasm' - Tempfile.create(['rubyboy-', '.wasm'], File.dirname(wasm_path)) do |output| - system(objcopy, '--strip-debug', wasm_path, output.path, exception: true) - FileUtils.mv(output.path, wasm_path) - end -end diff --git a/lib/bench.rb b/lib/bench.rb index fbd70cc..d919573 100644 --- a/lib/bench.rb +++ b/lib/bench.rb @@ -1,44 +1,27 @@ # frozen_string_literal: true -require 'json' require_relative 'rubyboy/emulator_headless' module Rubyboy class Bench - class WindowClosed < StandardError; end - - def run(count: 3, frames: 1500, rom_path: 'lib/roms/tobu.gb', warmup_frames: 0, render: false, json: false) + def run(count: 3, frames: 1500, rom_path: 'lib/roms/tobu.gb', warmup_frames: 0) validate_integer(:count, count, 1) validate_integer(:frames, frames, 1) validate_integer(:warmup_frames, warmup_frames, 0) - require_relative 'rubyboy/lcd' if render - trials = [] time_sum = 0 count.times do |i| - time = run_trial(rom_path, frames, warmup_frames, render) - elapsed_seconds = time / 1_000_000_000.0 - trials << { trial: i + 1, frames:, elapsed_seconds:, fps: frames / elapsed_seconds } - puts "#{i + 1}: #{elapsed_seconds} sec" unless json + emulator = Rubyboy::EmulatorHeadless.new(rom_path) + run_frames(emulator, warmup_frames) + start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) + run_frames(emulator, frames) + time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - start_time + puts "#{i + 1}: #{time / 1_000_000_000.0} sec" + time_sum += time end - result = { - schema_version: 1, - rom_path: File.expand_path(rom_path), - count:, - frames:, - warmup_frames:, - render:, - runtime:, - conditions: { apu: true, audio_output: false, realtime_sync: false, vsync: render ? false : nil, input: false, save: false }, - trials:, - total_frames: frames * count, - elapsed_seconds: time_sum / 1_000_000_000.0, - fps: frames * count * 1_000_000_000.0 / time_sum - } - puts(json ? JSON.generate(result) : "FPS: #{result[:fps]}") - result + puts "FPS: #{frames * count * 1_000_000_000.0 / time_sum}" end private @@ -50,33 +33,12 @@ def validate_integer(name, value, minimum) raise ArgumentError, "#{name} must be #{requirement}" end - def run_trial(rom_path, frames, warmup_frames, render) - emulator = Rubyboy::EmulatorHeadless.new(rom_path) - lcd = Rubyboy::Lcd.new(vsync: false) if render - begin - run_frames(emulator, lcd, warmup_frames) - start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - run_frames(emulator, lcd, frames) - Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - start_time - ensure - lcd&.close_window - end - end - - def run_frames(emulator, lcd, frames) + def run_frames(emulator, frames) frame_count = 0 while frame_count < frames - raise WindowClosed, 'Benchmark stopped because the window was closed.' if lcd&.window_should_close? - emulator.step - lcd&.draw(emulator.framebuffer) frame_count += 1 end end - - def runtime - yjit_enabled = defined?(RubyVM::YJIT) ? RubyVM::YJIT.enabled? : false - { engine: RUBY_ENGINE, version: RUBY_VERSION, description: RUBY_DESCRIPTION, platform: RUBY_PLATFORM, yjit_enabled: } - end end end diff --git a/lib/executor.rb b/lib/executor.rb index 324feed..53408a9 100644 --- a/lib/executor.rb +++ b/lib/executor.rb @@ -14,14 +14,6 @@ def exec(direction_key = 0b1111, action_key = 0b1111) @emulator.step(direction_key, action_key) write_framebuffer write_audio - 1 - end - - def exec_cycles(cycle_budget, direction_key = 0b1111, action_key = 0b1111) - frames = @emulator.run_cycles(cycle_budget, direction_key, action_key) - write_framebuffer if frames > 0 - write_audio - frames end def read_rom_from_virtual_fs diff --git a/lib/opcodes.json b/lib/opcodes.json deleted file mode 100644 index b962793..0000000 --- a/lib/opcodes.json +++ /dev/null @@ -1,11657 +0,0 @@ - - -{ - "source": "https://gbdev.io/gb-opcodes/Opcodes.json", - "unprefixed": { - "0x00": { - "mnemonic": "NOP", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x01": { - "mnemonic": "LD", - "bytes": 3, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "BC", - "immediate": true - }, - { - "name": "n16", - "bytes": 2, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x02": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "BC", - "immediate": false - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x03": { - "mnemonic": "INC", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "BC", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x04": { - "mnemonic": "INC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "-" - } - }, - "0x05": { - "mnemonic": "DEC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "-" - } - }, - "0x06": { - "mnemonic": "LD", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "B", - "immediate": true - }, - { - "name": "n8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x07": { - "mnemonic": "RLCA", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [], - "immediate": true, - "flags": { - "Z": "0", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x08": { - "mnemonic": "LD", - "bytes": 3, - "cycles": [ - 20 - ], - "operands": [ - { - "name": "a16", - "bytes": 2, - "immediate": false - }, - { - "name": "SP", - "immediate": true - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x09": { - "mnemonic": "ADD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "HL", - "immediate": true - }, - { - "name": "BC", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0x0A": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "BC", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x0B": { - "mnemonic": "DEC", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "BC", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x0C": { - "mnemonic": "INC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "-" - } - }, - "0x0D": { - "mnemonic": "DEC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "-" - } - }, - "0x0E": { - "mnemonic": "LD", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "C", - "immediate": true - }, - { - "name": "n8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x0F": { - "mnemonic": "RRCA", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [], - "immediate": true, - "flags": { - "Z": "0", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x10": { - "mnemonic": "STOP", - "bytes": 2, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "n8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x11": { - "mnemonic": "LD", - "bytes": 3, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "DE", - "immediate": true - }, - { - "name": "n16", - "bytes": 2, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x12": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "DE", - "immediate": false - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x13": { - "mnemonic": "INC", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "DE", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x14": { - "mnemonic": "INC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "-" - } - }, - "0x15": { - "mnemonic": "DEC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "-" - } - }, - "0x16": { - "mnemonic": "LD", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "D", - "immediate": true - }, - { - "name": "n8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x17": { - "mnemonic": "RLA", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [], - "immediate": true, - "flags": { - "Z": "0", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x18": { - "mnemonic": "JR", - "bytes": 2, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "e8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x19": { - "mnemonic": "ADD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "HL", - "immediate": true - }, - { - "name": "DE", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0x1A": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "DE", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x1B": { - "mnemonic": "DEC", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "DE", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x1C": { - "mnemonic": "INC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "-" - } - }, - "0x1D": { - "mnemonic": "DEC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "-" - } - }, - "0x1E": { - "mnemonic": "LD", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "E", - "immediate": true - }, - { - "name": "n8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x1F": { - "mnemonic": "RRA", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [], - "immediate": true, - "flags": { - "Z": "0", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x20": { - "mnemonic": "JR", - "bytes": 2, - "cycles": [ - 12, - 8 - ], - "operands": [ - { - "name": "NZ", - "immediate": true - }, - { - "name": "e8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x21": { - "mnemonic": "LD", - "bytes": 3, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "HL", - "immediate": true - }, - { - "name": "n16", - "bytes": 2, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x22": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "HL", - "increment": true, - "immediate": false - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x23": { - "mnemonic": "INC", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "HL", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x24": { - "mnemonic": "INC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "-" - } - }, - "0x25": { - "mnemonic": "DEC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "-" - } - }, - "0x26": { - "mnemonic": "LD", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "H", - "immediate": true - }, - { - "name": "n8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x27": { - "mnemonic": "DAA", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [], - "immediate": true, - "flags": { - "Z": "Z", - "N": "-", - "H": "0", - "C": "C" - } - }, - "0x28": { - "mnemonic": "JR", - "bytes": 2, - "cycles": [ - 12, - 8 - ], - "operands": [ - { - "name": "Z", - "immediate": true - }, - { - "name": "e8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x29": { - "mnemonic": "ADD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "HL", - "immediate": true - }, - { - "name": "HL", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0x2A": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "HL", - "increment": true, - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x2B": { - "mnemonic": "DEC", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "HL", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x2C": { - "mnemonic": "INC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "-" - } - }, - "0x2D": { - "mnemonic": "DEC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "-" - } - }, - "0x2E": { - "mnemonic": "LD", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "L", - "immediate": true - }, - { - "name": "n8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x2F": { - "mnemonic": "CPL", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [], - "immediate": true, - "flags": { - "Z": "-", - "N": "1", - "H": "1", - "C": "-" - } - }, - "0x30": { - "mnemonic": "JR", - "bytes": 2, - "cycles": [ - 12, - 8 - ], - "operands": [ - { - "name": "NC", - "immediate": true - }, - { - "name": "e8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x31": { - "mnemonic": "LD", - "bytes": 3, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "SP", - "immediate": true - }, - { - "name": "n16", - "bytes": 2, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x32": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "HL", - "decrement": true, - "immediate": false - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x33": { - "mnemonic": "INC", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "SP", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x34": { - "mnemonic": "INC", - "bytes": 1, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "-" - } - }, - "0x35": { - "mnemonic": "DEC", - "bytes": 1, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "-" - } - }, - "0x36": { - "mnemonic": "LD", - "bytes": 2, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "HL", - "immediate": false - }, - { - "name": "n8", - "bytes": 1, - "immediate": true - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x37": { - "mnemonic": "SCF", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [], - "immediate": true, - "flags": { - "Z": "-", - "N": "0", - "H": "0", - "C": "1" - } - }, - "0x38": { - "mnemonic": "JR", - "bytes": 2, - "cycles": [ - 12, - 8 - ], - "operands": [ - { - "name": "C", - "immediate": true - }, - { - "name": "e8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x39": { - "mnemonic": "ADD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "HL", - "immediate": true - }, - { - "name": "SP", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0x3A": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "HL", - "decrement": true, - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x3B": { - "mnemonic": "DEC", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "SP", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x3C": { - "mnemonic": "INC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "-" - } - }, - "0x3D": { - "mnemonic": "DEC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "-" - } - }, - "0x3E": { - "mnemonic": "LD", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "n8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x3F": { - "mnemonic": "CCF", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [], - "immediate": true, - "flags": { - "Z": "-", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x40": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "B", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x41": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "B", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x42": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "B", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x43": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "B", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x44": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "B", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x45": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "B", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x46": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "B", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x47": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "B", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x48": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "C", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x49": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "C", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x4A": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "C", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x4B": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "C", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x4C": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "C", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x4D": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "C", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x4E": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "C", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x4F": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "C", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x50": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "D", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x51": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "D", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x52": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "D", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x53": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "D", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x54": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "D", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x55": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "D", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x56": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "D", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x57": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "D", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x58": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "E", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x59": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "E", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x5A": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "E", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x5B": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "E", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x5C": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "E", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x5D": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "E", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x5E": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "E", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x5F": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "E", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x60": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "H", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x61": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "H", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x62": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "H", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x63": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "H", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x64": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "H", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x65": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "H", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x66": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "H", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x67": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "H", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x68": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "L", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x69": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "L", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x6A": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "L", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x6B": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "L", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x6C": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "L", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x6D": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "L", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x6E": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "L", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x6F": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "L", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x70": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "HL", - "immediate": false - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x71": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "HL", - "immediate": false - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x72": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "HL", - "immediate": false - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x73": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "HL", - "immediate": false - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x74": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "HL", - "immediate": false - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x75": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "HL", - "immediate": false - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x76": { - "mnemonic": "HALT", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x77": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "HL", - "immediate": false - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x78": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x79": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x7A": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x7B": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x7C": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x7D": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x7E": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x7F": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x80": { - "mnemonic": "ADD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0x81": { - "mnemonic": "ADD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0x82": { - "mnemonic": "ADD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0x83": { - "mnemonic": "ADD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0x84": { - "mnemonic": "ADD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0x85": { - "mnemonic": "ADD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0x86": { - "mnemonic": "ADD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0x87": { - "mnemonic": "ADD", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0x88": { - "mnemonic": "ADC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0x89": { - "mnemonic": "ADC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0x8A": { - "mnemonic": "ADC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0x8B": { - "mnemonic": "ADC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0x8C": { - "mnemonic": "ADC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0x8D": { - "mnemonic": "ADC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0x8E": { - "mnemonic": "ADC", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0x8F": { - "mnemonic": "ADC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0x90": { - "mnemonic": "SUB", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0x91": { - "mnemonic": "SUB", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0x92": { - "mnemonic": "SUB", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0x93": { - "mnemonic": "SUB", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0x94": { - "mnemonic": "SUB", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0x95": { - "mnemonic": "SUB", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0x96": { - "mnemonic": "SUB", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0x97": { - "mnemonic": "SUB", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "1", - "N": "1", - "H": "0", - "C": "0" - } - }, - "0x98": { - "mnemonic": "SBC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0x99": { - "mnemonic": "SBC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0x9A": { - "mnemonic": "SBC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0x9B": { - "mnemonic": "SBC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0x9C": { - "mnemonic": "SBC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0x9D": { - "mnemonic": "SBC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0x9E": { - "mnemonic": "SBC", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0x9F": { - "mnemonic": "SBC", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "-" - } - }, - "0xA0": { - "mnemonic": "AND", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "0" - } - }, - "0xA1": { - "mnemonic": "AND", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "0" - } - }, - "0xA2": { - "mnemonic": "AND", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "0" - } - }, - "0xA3": { - "mnemonic": "AND", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "0" - } - }, - "0xA4": { - "mnemonic": "AND", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "0" - } - }, - "0xA5": { - "mnemonic": "AND", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "0" - } - }, - "0xA6": { - "mnemonic": "AND", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "0" - } - }, - "0xA7": { - "mnemonic": "AND", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "0" - } - }, - "0xA8": { - "mnemonic": "XOR", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0xA9": { - "mnemonic": "XOR", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0xAA": { - "mnemonic": "XOR", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0xAB": { - "mnemonic": "XOR", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0xAC": { - "mnemonic": "XOR", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0xAD": { - "mnemonic": "XOR", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0xAE": { - "mnemonic": "XOR", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0xAF": { - "mnemonic": "XOR", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "1", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0xB0": { - "mnemonic": "OR", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0xB1": { - "mnemonic": "OR", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0xB2": { - "mnemonic": "OR", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0xB3": { - "mnemonic": "OR", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0xB4": { - "mnemonic": "OR", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0xB5": { - "mnemonic": "OR", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0xB6": { - "mnemonic": "OR", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0xB7": { - "mnemonic": "OR", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0xB8": { - "mnemonic": "CP", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0xB9": { - "mnemonic": "CP", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0xBA": { - "mnemonic": "CP", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0xBB": { - "mnemonic": "CP", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0xBC": { - "mnemonic": "CP", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0xBD": { - "mnemonic": "CP", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0xBE": { - "mnemonic": "CP", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0xBF": { - "mnemonic": "CP", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "1", - "N": "1", - "H": "0", - "C": "0" - } - }, - "0xC0": { - "mnemonic": "RET", - "bytes": 1, - "cycles": [ - 20, - 8 - ], - "operands": [ - { - "name": "NZ", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xC1": { - "mnemonic": "POP", - "bytes": 1, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "BC", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xC2": { - "mnemonic": "JP", - "bytes": 3, - "cycles": [ - 16, - 12 - ], - "operands": [ - { - "name": "NZ", - "immediate": true - }, - { - "name": "a16", - "bytes": 2, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xC3": { - "mnemonic": "JP", - "bytes": 3, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "a16", - "bytes": 2, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xC4": { - "mnemonic": "CALL", - "bytes": 3, - "cycles": [ - 24, - 12 - ], - "operands": [ - { - "name": "NZ", - "immediate": true - }, - { - "name": "a16", - "bytes": 2, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xC5": { - "mnemonic": "PUSH", - "bytes": 1, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "BC", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xC6": { - "mnemonic": "ADD", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "n8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0xC7": { - "mnemonic": "RST", - "bytes": 1, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "$00", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xC8": { - "mnemonic": "RET", - "bytes": 1, - "cycles": [ - 20, - 8 - ], - "operands": [ - { - "name": "Z", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xC9": { - "mnemonic": "RET", - "bytes": 1, - "cycles": [ - 16 - ], - "operands": [], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xCA": { - "mnemonic": "JP", - "bytes": 3, - "cycles": [ - 16, - 12 - ], - "operands": [ - { - "name": "Z", - "immediate": true - }, - { - "name": "a16", - "bytes": 2, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xCB": { - "mnemonic": "PREFIX", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xCC": { - "mnemonic": "CALL", - "bytes": 3, - "cycles": [ - 24, - 12 - ], - "operands": [ - { - "name": "Z", - "immediate": true - }, - { - "name": "a16", - "bytes": 2, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xCD": { - "mnemonic": "CALL", - "bytes": 3, - "cycles": [ - 24 - ], - "operands": [ - { - "name": "a16", - "bytes": 2, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xCE": { - "mnemonic": "ADC", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "n8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0xCF": { - "mnemonic": "RST", - "bytes": 1, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "$08", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xD0": { - "mnemonic": "RET", - "bytes": 1, - "cycles": [ - 20, - 8 - ], - "operands": [ - { - "name": "NC", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xD1": { - "mnemonic": "POP", - "bytes": 1, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "DE", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xD2": { - "mnemonic": "JP", - "bytes": 3, - "cycles": [ - 16, - 12 - ], - "operands": [ - { - "name": "NC", - "immediate": true - }, - { - "name": "a16", - "bytes": 2, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xD3": { - "mnemonic": "ILLEGAL_D3", - "bytes": 1, - "cycles": [ 4 ], - "operands": [ ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xD4": { - "mnemonic": "CALL", - "bytes": 3, - "cycles": [ - 24, - 12 - ], - "operands": [ - { - "name": "NC", - "immediate": true - }, - { - "name": "a16", - "bytes": 2, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xD5": { - "mnemonic": "PUSH", - "bytes": 1, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "DE", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xD6": { - "mnemonic": "SUB", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "n8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0xD7": { - "mnemonic": "RST", - "bytes": 1, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "$10", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xD8": { - "mnemonic": "RET", - "bytes": 1, - "cycles": [ - 20, - 8 - ], - "operands": [ - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xD9": { - "mnemonic": "RETI", - "bytes": 1, - "cycles": [ - 16 - ], - "operands": [], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xDA": { - "mnemonic": "JP", - "bytes": 3, - "cycles": [ - 16, - 12 - ], - "operands": [ - { - "name": "C", - "immediate": true - }, - { - "name": "a16", - "bytes": 2, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xDB": { - "mnemonic": "ILLEGAL_DB", - "bytes": 1, - "cycles": [ 4 ], - "operands": [ ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xDC": { - "mnemonic": "CALL", - "bytes": 3, - "cycles": [ - 24, - 12 - ], - "operands": [ - { - "name": "C", - "immediate": true - }, - { - "name": "a16", - "bytes": 2, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xDD": { - "mnemonic": "ILLEGAL_DD", - "bytes": 1, - "cycles": [ 4 ], - "operands": [ ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xDE": { - "mnemonic": "SBC", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "n8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0xDF": { - "mnemonic": "RST", - "bytes": 1, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "$18", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xE0": { - "mnemonic": "LDH", - "bytes": 2, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "a8", - "bytes": 1, - "immediate": false - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xE1": { - "mnemonic": "POP", - "bytes": 1, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "HL", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xE2": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "C", - "immediate": false - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xE3": { - "mnemonic": "ILLEGAL_E3", - "bytes": 1, - "cycles": [ 4 ], - "operands": [ ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xE4": { - "mnemonic": "ILLEGAL_E4", - "bytes": 1, - "cycles": [ 4 ], - "operands": [ ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xE5": { - "mnemonic": "PUSH", - "bytes": 1, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "HL", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xE6": { - "mnemonic": "AND", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "n8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "0" - } - }, - "0xE7": { - "mnemonic": "RST", - "bytes": 1, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "$20", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xE8": { - "mnemonic": "ADD", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "SP", - "immediate": true - }, - { - "name": "e8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "0", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0xE9": { - "mnemonic": "JP", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [ - { - "name": "HL", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xEA": { - "mnemonic": "LD", - "bytes": 3, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "a16", - "bytes": 2, - "immediate": false - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xEB": { - "mnemonic": "ILLEGAL_EB", - "bytes": 1, - "cycles": [ 4 ], - "operands": [ ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xEC": { - "mnemonic": "ILLEGAL_EC", - "bytes": 1, - "cycles": [ 4 ], - "operands": [ ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xED": { - "mnemonic": "ILLEGAL_ED", - "bytes": 1, - "cycles": [ 4 ], - "operands": [ ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xEE": { - "mnemonic": "XOR", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "n8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0xEF": { - "mnemonic": "RST", - "bytes": 1, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "$28", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xF0": { - "mnemonic": "LDH", - "bytes": 2, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "a8", - "bytes": 1, - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xF1": { - "mnemonic": "POP", - "bytes": 1, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "AF", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "N", - "H": "H", - "C": "C" - } - }, - "0xF2": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "C", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xF3": { - "mnemonic": "DI", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xF4": { - "mnemonic": "ILLEGAL_F4", - "bytes": 1, - "cycles": [ 4 ], - "operands": [ ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xF5": { - "mnemonic": "PUSH", - "bytes": 1, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "AF", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xF6": { - "mnemonic": "OR", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "n8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0xF7": { - "mnemonic": "RST", - "bytes": 1, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "$30", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xF8": { - "mnemonic": "LD", - "bytes": 2, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "HL", - "immediate": true - }, - { - "name": "SP", - "increment": true, - "immediate": true - }, - { - "name": "e8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "0", - "N": "0", - "H": "H", - "C": "C" - } - }, - "0xF9": { - "mnemonic": "LD", - "bytes": 1, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "SP", - "immediate": true - }, - { - "name": "HL", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xFA": { - "mnemonic": "LD", - "bytes": 3, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "a16", - "bytes": 2, - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xFB": { - "mnemonic": "EI", - "bytes": 1, - "cycles": [ - 4 - ], - "operands": [], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xFC": { - "mnemonic": "ILLEGAL_FC", - "bytes": 1, - "cycles": [ 4 ], - "operands": [ ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xFD": { - "mnemonic": "ILLEGAL_FD", - "bytes": 1, - "cycles": [ 4 ], - "operands": [ ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xFE": { - "mnemonic": "CP", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - }, - { - "name": "n8", - "bytes": 1, - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "1", - "H": "H", - "C": "C" - } - }, - "0xFF": { - "mnemonic": "RST", - "bytes": 1, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "$38", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - } - }, - "cbprefixed": { - "0x00": { - "mnemonic": "RLC", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x01": { - "mnemonic": "RLC", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x02": { - "mnemonic": "RLC", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x03": { - "mnemonic": "RLC", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x04": { - "mnemonic": "RLC", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x05": { - "mnemonic": "RLC", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x06": { - "mnemonic": "RLC", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x07": { - "mnemonic": "RLC", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x08": { - "mnemonic": "RRC", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x09": { - "mnemonic": "RRC", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x0A": { - "mnemonic": "RRC", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x0B": { - "mnemonic": "RRC", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x0C": { - "mnemonic": "RRC", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x0D": { - "mnemonic": "RRC", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x0E": { - "mnemonic": "RRC", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x0F": { - "mnemonic": "RRC", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x10": { - "mnemonic": "RL", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x11": { - "mnemonic": "RL", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x12": { - "mnemonic": "RL", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x13": { - "mnemonic": "RL", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x14": { - "mnemonic": "RL", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x15": { - "mnemonic": "RL", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x16": { - "mnemonic": "RL", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x17": { - "mnemonic": "RL", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x18": { - "mnemonic": "RR", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x19": { - "mnemonic": "RR", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x1A": { - "mnemonic": "RR", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x1B": { - "mnemonic": "RR", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x1C": { - "mnemonic": "RR", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x1D": { - "mnemonic": "RR", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x1E": { - "mnemonic": "RR", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x1F": { - "mnemonic": "RR", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x20": { - "mnemonic": "SLA", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x21": { - "mnemonic": "SLA", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x22": { - "mnemonic": "SLA", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x23": { - "mnemonic": "SLA", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x24": { - "mnemonic": "SLA", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x25": { - "mnemonic": "SLA", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x26": { - "mnemonic": "SLA", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x27": { - "mnemonic": "SLA", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x28": { - "mnemonic": "SRA", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x29": { - "mnemonic": "SRA", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x2A": { - "mnemonic": "SRA", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x2B": { - "mnemonic": "SRA", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x2C": { - "mnemonic": "SRA", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x2D": { - "mnemonic": "SRA", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x2E": { - "mnemonic": "SRA", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x2F": { - "mnemonic": "SRA", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x30": { - "mnemonic": "SWAP", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0x31": { - "mnemonic": "SWAP", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0x32": { - "mnemonic": "SWAP", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0x33": { - "mnemonic": "SWAP", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0x34": { - "mnemonic": "SWAP", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0x35": { - "mnemonic": "SWAP", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0x36": { - "mnemonic": "SWAP", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0x37": { - "mnemonic": "SWAP", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "0" - } - }, - "0x38": { - "mnemonic": "SRL", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x39": { - "mnemonic": "SRL", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x3A": { - "mnemonic": "SRL", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x3B": { - "mnemonic": "SRL", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x3C": { - "mnemonic": "SRL", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x3D": { - "mnemonic": "SRL", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x3E": { - "mnemonic": "SRL", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x3F": { - "mnemonic": "SRL", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "0", - "C": "C" - } - }, - "0x40": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x41": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x42": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x43": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x44": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x45": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x46": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x47": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x48": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x49": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x4A": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x4B": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x4C": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x4D": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x4E": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x4F": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x50": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x51": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x52": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x53": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x54": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x55": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x56": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x57": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x58": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x59": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x5A": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x5B": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x5C": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x5D": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x5E": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x5F": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x60": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x61": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x62": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x63": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x64": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x65": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x66": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x67": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x68": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x69": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x6A": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x6B": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x6C": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x6D": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x6E": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x6F": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x70": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x71": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x72": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x73": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x74": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x75": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x76": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x77": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x78": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x79": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x7A": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x7B": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x7C": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x7D": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x7E": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 12 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x7F": { - "mnemonic": "BIT", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "Z", - "N": "0", - "H": "1", - "C": "-" - } - }, - "0x80": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x81": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x82": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x83": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x84": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x85": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x86": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x87": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x88": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x89": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x8A": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x8B": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x8C": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x8D": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x8E": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x8F": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x90": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x91": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x92": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x93": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x94": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x95": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x96": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x97": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x98": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x99": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x9A": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x9B": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x9C": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x9D": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x9E": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0x9F": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xA0": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xA1": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xA2": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xA3": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xA4": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xA5": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xA6": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xA7": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xA8": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xA9": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xAA": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xAB": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xAC": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xAD": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xAE": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xAF": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xB0": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xB1": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xB2": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xB3": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xB4": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xB5": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xB6": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xB7": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xB8": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xB9": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xBA": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xBB": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xBC": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xBD": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xBE": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xBF": { - "mnemonic": "RES", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xC0": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xC1": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xC2": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xC3": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xC4": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xC5": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xC6": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xC7": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "0", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xC8": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xC9": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xCA": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xCB": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xCC": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xCD": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xCE": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xCF": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "1", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xD0": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xD1": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xD2": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xD3": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xD4": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xD5": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xD6": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xD7": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "2", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xD8": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xD9": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xDA": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xDB": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xDC": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xDD": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xDE": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xDF": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "3", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xE0": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xE1": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xE2": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xE3": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xE4": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xE5": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xE6": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xE7": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "4", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xE8": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xE9": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xEA": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xEB": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xEC": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xED": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xEE": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xEF": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "5", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xF0": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xF1": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xF2": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xF3": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xF4": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xF5": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xF6": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xF7": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "6", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xF8": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "B", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xF9": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "C", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xFA": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "D", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xFB": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "E", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xFC": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "H", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xFD": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "L", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xFE": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 16 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "HL", - "immediate": false - } - ], - "immediate": false, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - }, - "0xFF": { - "mnemonic": "SET", - "bytes": 2, - "cycles": [ - 8 - ], - "operands": [ - { - "name": "7", - "immediate": true - }, - { - "name": "A", - "immediate": true - } - ], - "immediate": true, - "flags": { - "Z": "-", - "N": "-", - "H": "-", - "C": "-" - } - } - } -} diff --git a/lib/rubyboy/audio.rb b/lib/rubyboy/audio.rb index bf36191..e070489 100644 --- a/lib/rubyboy/audio.rb +++ b/lib/rubyboy/audio.rb @@ -21,14 +21,19 @@ def initialize SDL.PauseAudioDevice(@device, 0) end - def queue(buffer) + def queue(buffer, wait: true) # Block until the SDL audio queue has drained enough to accept another # buffer. The previous implementation called SDL.ClearQueuedAudio here # instead, which dropped the entire queue whenever the emulator ran ahead # of real time — that produced audible pops/gaps in BGM (most noticeable # in Pokemon-style tracks). Blocking here costs a tiny bit of frame # budget but keeps audio continuous. - sleep(0.001) while SDL.GetQueuedAudioSize(@device) > 8192 + # Unlimited playback drops this buffer instead, so audio never slows it down. + while SDL.GetQueuedAudioSize(@device) > 8192 + return unless wait + + sleep(0.001) + end buf_ptr = FFI::MemoryPointer.new(:float, buffer.size) buf_ptr.put_array_of_float(0, buffer) diff --git a/lib/rubyboy/emulator.rb b/lib/rubyboy/emulator.rb index 50f2ead..b391d20 100644 --- a/lib/rubyboy/emulator.rb +++ b/lib/rubyboy/emulator.rb @@ -17,7 +17,7 @@ class Emulator SDL::SDL_SCANCODE_0 ].freeze - def initialize(rom_path, audio: true) + def initialize(rom_path) @rom_path = rom_path rom_data = File.open(rom_path, 'r') { _1.read.bytes } @rom = Rom.new(rom_data) @@ -31,7 +31,7 @@ def initialize(rom_path, audio: true) @bus = Bus.new(@ppu, @rom, @ram, @mbc, @timer, @interrupt, @joypad, @apu) @cpu = Cpu.new(@bus, @interrupt) @lcd = Lcd.new - @audio = audio ? Audio.new : nil + @audio = Audio.new @prev_save_state_keys = Array.new(SAVE_STATE_KEYS.size, 0) @save_file = @rom.battery? ? SaveFile.new(default_save_path(rom_path)) : nil load_save_file @@ -52,7 +52,7 @@ def start(frames: nil, realtime: true) while !realtime || elapsed_real_time > elapsed_machine_time cycles = @cpu.exec @timer.step(cycles) - @audio.queue(@apu.samples) if @apu.step(cycles) && @audio + @audio.queue(@apu.samples, wait: realtime) if @apu.step(cycles) if @ppu.step(cycles) @lcd.draw(@ppu.buffer) key_input_check @@ -74,22 +74,6 @@ def start(frames: nil, realtime: true) end end - def bench(frames) - @lcd.close_window - frame_count = 0 - start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - while frame_count < frames - cycles = @cpu.exec - @timer.step(cycles) - if @ppu.step(cycles) - key_input_check - frame_count += 1 - end - end - - Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - start_time - end - def save_state(slot: nil, path: nil) state_path = path || slot_path(slot) return false unless StateFile.write(state_path, rom: @rom) { hardware_state } @@ -102,7 +86,7 @@ def load_state(slot: nil, path: nil) state_path = path || slot_path(slot) return false unless StateFile.read(state_path, rom: @rom) { |state| restore_hardware_state(state) } - @audio&.clear_queue + @audio.clear_queue puts "Loaded state from #{state_path}" true end diff --git a/lib/rubyboy/emulator_headless.rb b/lib/rubyboy/emulator_headless.rb index 21cf849..caa1faf 100644 --- a/lib/rubyboy/emulator_headless.rb +++ b/lib/rubyboy/emulator_headless.rb @@ -27,10 +27,6 @@ def initialize(rom_path) @cpu = Cpu.new(bus, interrupt) end - def framebuffer - @ppu.buffer - end - def step loop do cycles = @cpu.exec diff --git a/lib/rubyboy/emulator_wasm.rb b/lib/rubyboy/emulator_wasm.rb index 53ea025..3666036 100644 --- a/lib/rubyboy/emulator_wasm.rb +++ b/lib/rubyboy/emulator_wasm.rb @@ -13,8 +13,6 @@ module Rubyboy class EmulatorWasm - CPU_CLOCK_HZ = 4_194_304 - CYCLE_NANOSEC = 1_000_000_000 / CPU_CLOCK_HZ FRAME_CYCLES = 70_224 attr_reader :audio_samples @@ -31,7 +29,6 @@ def initialize(rom_data) @bus = Bus.new(@ppu, rom, ram, mbc, @timer, interrupt, @joypad, @apu) @cpu = Cpu.new(@bus, interrupt) @audio_samples = [] - @cycle_debt = 0 end def step(direction_key, action_key) @@ -49,25 +46,6 @@ def step(direction_key, action_key) framebuffer end - # CPU instructions are indivisible. Carry their overshoot into the next - # budget so small browser ticks do not make the emulated clock run faster. - def run_cycles(cycle_budget, direction_key, action_key) - raise ArgumentError, 'cycle budget must be a nonnegative integer' unless cycle_budget.is_a?(Integer) && cycle_budget >= 0 - - set_input(direction_key, action_key) - @audio_samples.clear - @cycle_debt += cycle_budget - frames = 0 - while @cycle_debt > 0 - cycles = @cpu.exec - @cycle_debt -= cycles - @timer.step(cycles) - collect_audio(cycles) - frames += 1 if @ppu.step(cycles) - end - frames - end - def framebuffer @ppu.buffer end diff --git a/lib/rubyboy/hardware_state.rb b/lib/rubyboy/hardware_state.rb index ba527e1..956d2c3 100644 --- a/lib/rubyboy/hardware_state.rb +++ b/lib/rubyboy/hardware_state.rb @@ -173,6 +173,7 @@ def rebuild_render_caches end end + # Keep channel fields explicit so Spinel can compile them without reflection. module ApuChannels class Channel1 def hardware_state diff --git a/lib/rubyboy/lcd.rb b/lib/rubyboy/lcd.rb index 51dc983..3ac9c80 100644 --- a/lib/rubyboy/lcd.rb +++ b/lib/rubyboy/lcd.rb @@ -9,7 +9,7 @@ class Lcd SCALE = 3 SDL_EVENT_SIZE = 56 - def initialize(vsync: nil) + def initialize raise SDL.GetError() if SDL.InitSubSystem(SDL::INIT_VIDEO) != 0 @buffer = FFI::MemoryPointer.new(:uint32, SCREEN_WIDTH * SCREEN_HEIGHT) @@ -17,15 +17,7 @@ def initialize(vsync: nil) raise SDL.GetError() if @window.null? - begin - @renderer = SDL.CreateRenderer(@window, -1, 0) - raise SDL.GetError() if @renderer.null? - raise SDL.GetError() if !vsync.nil? && SDL.RenderSetVSync(@renderer, vsync ? 1 : 0) != 0 - rescue StandardError - close_window - raise - end - + @renderer = SDL.CreateRenderer(@window, -1, 0) SDL.SetHint('SDL_HINT_RENDER_SCALE_QUALITY', '2') SDL.RenderSetLogicalSize(@renderer, SCREEN_WIDTH * SCALE, SCREEN_HEIGHT * SCALE) @texture = SDL.CreateTexture(@renderer, SDL::PIXELFORMAT_ABGR8888, 1, SCREEN_WIDTH, SCREEN_HEIGHT) diff --git a/lib/rubyboy/raylib/audio.rb b/lib/rubyboy/raylib/audio.rb deleted file mode 100644 index bc9fef5..0000000 --- a/lib/rubyboy/raylib/audio.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -require 'raylib' - -module Rubyboy - module Raylib - class Audio - include Raylib - - MAX_SAMPLES = 512 - - def initialize - InitAudioDevice() - SetAudioStreamBufferSizeDefault(MAX_SAMPLES * 2) - @stream = LoadAudioStream(48000, 32, 2) - PlayAudioStream(@stream) - end - - def play(samples) - samples_pointer = FFI::MemoryPointer.new(:float, samples.size) - samples_pointer.put_array_of_float(0, samples) - - UpdateAudioStream(@stream, samples_pointer, samples.size) - end - - def close - UnloadAudioStream(@stream) - CloseAudioDevice() - end - end - end -end diff --git a/lib/rubyboy/raylib/lcd.rb b/lib/rubyboy/raylib/lcd.rb deleted file mode 100644 index 28ef439..0000000 --- a/lib/rubyboy/raylib/lcd.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -require 'raylib' - -module Rubyboy - module Raylib - class Lcd - include Raylib - - WIDTH = 160 - HEIGHT = 144 - SCALE = 4 - - def initialize - InitWindow(WIDTH * SCALE, HEIGHT * SCALE, 'Ruby Boy') - image = GenImageColor(WIDTH, HEIGHT, BLACK) - image.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8 - @texture = LoadTextureFromImage(image) - @vector = Vector2.create(0, 0) - end - - def draw(pixel_data) - UpdateTexture(@texture, pixel_data.pack('C*')) - - BeginDrawing() - ClearBackground(BLACK) - DrawTextureEx(@texture, @vector, 0.0, SCALE, WHITE) - EndDrawing() - end - - def window_should_close? - WindowShouldClose() - end - - def close_window - CloseWindow() - end - end - end -end diff --git a/lib/rubyboy/raylib/raylib_loader.rb b/lib/rubyboy/raylib/raylib_loader.rb deleted file mode 100644 index 6f08173..0000000 --- a/lib/rubyboy/raylib/raylib_loader.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -require 'raylib' - -module Rubyboy - module Raylib - class RaylibLoader - include Raylib - - def load_raylib - shared_lib_path = "#{Gem::Specification.find_by_name('raylib-bindings').full_gem_path}/lib/" - case RUBY_PLATFORM - when /mswin|msys|mingw/ # Windows - Raylib.load_lib("#{shared_lib_path}libraylib.dll") - when /darwin/ # macOS - arch = RUBY_PLATFORM.split('-')[0] - Raylib.load_lib(shared_lib_path + "libraylib.#{arch}.dylib") - when /linux/ # Ubuntu Linux (x86_64 or aarch64) - arch = RUBY_PLATFORM.split('-')[0] - Raylib.load_lib(shared_lib_path + "libraylib.#{arch}.so") - else - raise "Unknown system: #{RUBY_PLATFORM}" - end - - SetTraceLogLevel(LOG_ERROR) - end - - def key_input_check - direction = (IsKeyUp(KEY_D) && 1 || 0) | ((IsKeyUp(KEY_A) && 1 || 0) << 1) | ((IsKeyUp(KEY_W) && 1 || 0) << 2) | ((IsKeyUp(KEY_S) && 1 || 0) << 3) - action = (IsKeyUp(KEY_K) && 1 || 0) | ((IsKeyUp(KEY_J) && 1 || 0) << 1) | ((IsKeyUp(KEY_U) && 1 || 0) << 2) | ((IsKeyUp(KEY_I) && 1 || 0) << 3) - @joypad.direction_button(direction) - @joypad.action_button(action) - end - end - end -end diff --git a/lib/rubyboy/sdl.rb b/lib/rubyboy/sdl.rb index 649a281..03fb53f 100644 --- a/lib/rubyboy/sdl.rb +++ b/lib/rubyboy/sdl.rb @@ -53,7 +53,6 @@ module SDL attach_function :GetKeyboardState, 'SDL_GetKeyboardState', [:pointer], :pointer attach_function :SetHint, 'SDL_SetHint', %i[string string], :int attach_function :RenderSetLogicalSize, 'SDL_RenderSetLogicalSize', %i[pointer int int], :int - attach_function :RenderSetVSync, 'SDL_RenderSetVSync', %i[pointer int], :int attach_function :SetWindowTitle, 'SDL_SetWindowTitle', %i[pointer string], :void attach_function :RaiseWindow, 'SDL_RaiseWindow', [:pointer], :void attach_function :GetError, 'SDL_GetError', [], :string diff --git a/lib/stackprof.rb b/lib/stackprof.rb deleted file mode 100644 index c0dbc48..0000000 --- a/lib/stackprof.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require 'stackprof' -require_relative 'rubyboy/emulator' - -module Rubyboy - class Stackprof - def run - StackProf.run(mode: :cpu, out: 'stackprof-cpu-myapp.dump', raw: true) do - Rubyboy::Emulator.new('lib/roms/tobu.gb').bench - end - end - end -end diff --git a/rubyboy.gemspec b/rubyboy.gemspec index 4229a9a..4f5d010 100644 --- a/rubyboy.gemspec +++ b/rubyboy.gemspec @@ -24,16 +24,15 @@ Gem::Specification.new do |spec| spec.files = Dir.chdir(__dir__) do `git ls-files -z`.split("\x0").reject do |f| (File.expand_path(f) == __FILE__) || - f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile]) + f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile docs/ spinel/ wasm/]) end end spec.bindir = 'exe' - spec.executables = %w[rubyboy rubyboy-bench rubyboy-wasm] + spec.executables = %w[rubyboy rubyboy-bench] spec.require_paths = ['lib'] # Uncomment to register a new dependency of your gem spec.add_dependency 'ffi', '~> 1.16', '>= 1.16.3' - spec.add_dependency 'json', '~> 2.8' # For more information and examples about making a new gem, check out our # guide at: https://bundler.io/guides/creating_gem.html diff --git a/scripts/compare_wasm_optimization.mjs b/scripts/compare_wasm_optimization.mjs deleted file mode 100644 index 1f5e360..0000000 --- a/scripts/compare_wasm_optimization.mjs +++ /dev/null @@ -1,278 +0,0 @@ -// Fixed comparison, prepared before observing results. No source or UI changes. -// node scripts/compare_wasm_optimization.mjs BASELINE.wasm CANDIDATE.wasm [OUTPUT.json] -import assert from 'node:assert/strict'; -import { createHash, webcrypto } from 'node:crypto'; -import { lstatSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; -import { spawnSync } from 'node:child_process'; -import { performance } from 'node:perf_hooks'; -import { arch, cpus, platform, release } from 'node:os'; -import { resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { DefaultRubyVM } from '../build/browser-runtime/node_modules/@ruby/wasm-wasi/dist/esm/browser.js'; -import { File } from '../build/browser-runtime/node_modules/@bjorn3/browser_wasi_shim/dist/index.js'; -import { RubyboyVM } from '../docs/rubyboy-vm.js'; - -if (!globalThis.crypto) globalThis.crypto = webcrypto; -const REPO = resolve(fileURLToPath(new URL('../', import.meta.url))); -const SCRIPT = fileURLToPath(import.meta.url); -const LIB_PATH = `${REPO}/lib`; -const ROM_PATH = `${REPO}/lib/roms/tobu.gb`; -const SDK_PACKAGE_PATH = `${REPO}/build/browser-runtime/node_modules/@ruby/wasm-wasi/package.json`; -const SHIM_PACKAGE_PATH = `${REPO}/build/browser-runtime/node_modules/@bjorn3/browser_wasi_shim/package.json`; -const ADAPTER_PATH = `${REPO}/docs/rubyboy-vm.js`; -const TRIAL_MARKER = 'RUBYBOY_OPT_TRIAL_JSON '; -const CONDITIONS = Object.freeze({ - warmup_run_frame_calls: 300, - measured_run_frame_calls: 600, - paired_trials: 5, - orders: [['baseline', 'candidate'], ['candidate', 'baseline'], ['baseline', 'candidate'], ['candidate', 'baseline'], ['baseline', 'candidate']], - direction_mask: 15, - action_mask: 15, - apu_enabled: true, - transfer_audio: true, - transfer_framebuffer: true, - timed_operations: 'core.runFrame -> core.popAudio -> transfer audio -> core.framebuffer -> transfer framebuffer', - timer: 'performance.now; one timer around all 600 calls and their VFS copies/transfers', - hash_work_in_timed_region: false, - rom: ROM_PATH, - packed_sources: '/lib', - ruby_version: '4.0.7', - sdk_version: '2.10.1', - wasi_shim_version: '0.4.2', - fresh_process_and_default_ruby_vm_per_trial: true, - concurrent_trials: false, - candidate_command: 'wasm-opt baseline --strip-dwarf -O4 --converge -g -o candidate', - acceptance: 'All final framebuffer, full measured Float32 stereo audio, and final Marshal state hashes match across every trial; all five candidate trials are faster than their paired baseline; median paired FPS speedup is at least 5%. Otherwise retain baseline.', - min_median_paired_speedup_percent: 5, - cold_measurement_scope: 'First WebAssembly.compile in each fresh Node process; VM init, adapter construction and ROM upload timed separately. Node startup, JS imports, file reads, and network transfer excluded.', -}); - -const hash = bytes => createHash('sha256').update(new Uint8Array(bytes)).digest('hex'); -const median = values => [...values].sort((a, b) => a - b)[Math.floor(values.length / 2)]; - -function fixtures() { - const sdkPackage = JSON.parse(readFileSync(SDK_PACKAGE_PATH)); - const shimPackage = JSON.parse(readFileSync(SHIM_PACKAGE_PATH)); - assert.equal(sdkPackage.version, CONDITIONS.sdk_version, 'Pin the original Ruby SDK to 2.10.1'); - assert.equal(shimPackage.version, CONDITIONS.wasi_shim_version, 'Pin the original WASI shim to 0.4.2'); - function sourceFiles(directory, prefix = '') { - return readdirSync(directory).sort().flatMap(name => { - const path = `${directory}/${name}`; - const relative = `${prefix}${name}`; - if (lstatSync(path).isDirectory()) return sourceFiles(path, `${relative}/`); - return name.endsWith('.rb') ? [relative] : []; - }); - } - const sourceHashes = Object.fromEntries(sourceFiles(LIB_PATH).map(path => [path, hash(readFileSync(`${LIB_PATH}/${path}`))])); - const romBytes = readFileSync(ROM_PATH); - return { - romBytes, - metadata: { - source_hashes: sourceHashes, - rom_sha256: hash(romBytes), - adapter_sha256: hash(readFileSync(ADAPTER_PATH)), - benchmark_script_sha256: hash(readFileSync(SCRIPT)), - sdk: { name: sdkPackage.name, version: sdkPackage.version, package_sha256: hash(readFileSync(SDK_PACKAGE_PATH)) }, - wasi_shim: { name: shimPackage.name, version: shimPackage.version, package_sha256: hash(readFileSync(SHIM_PACKAGE_PATH)) }, - }, - }; -} - -async function trial(label, pair, wasmPath) { - const { romBytes, metadata } = fixtures(); - const wasmBytes = readFileSync(wasmPath); - const compileStart = performance.now(); - const wasmModule = await WebAssembly.compile(wasmBytes); - const compileMs = performance.now() - compileStart; - const initStart = performance.now(); - const { vm, wasi } = await DefaultRubyVM(wasmModule); - const root = wasi.fds[3].dir; - const core = new RubyboyVM(vm, root, File); - core.loadUploadedRom(Uint8Array.from(romBytes).buffer); - const initMs = performance.now() - initStart; - - const rubyVersion = vm.eval('RUBY_VERSION').toString(); - assert.equal(rubyVersion, CONDITIONS.ruby_version); - const runtime = { - description: vm.eval('RUBY_DESCRIPTION').toString(), - version: rubyVersion, - platform: vm.eval('RUBY_PLATFORM').toString(), - yjit_enabled: vm.eval('defined?(RubyVM::YJIT) ? RubyVM::YJIT.enabled? : false').toString() === 'true', - zjit_enabled: vm.eval('defined?(RubyVM::ZJIT) ? RubyVM::ZJIT.enabled? : false').toString() === 'true', - executor_source: vm.eval('Executor.instance_method(:exec).source_location.first').toString(), - }; - assert.equal(runtime.yjit_enabled, false); - assert.equal(runtime.zjit_enabled, false); - assert.equal(runtime.executor_source, '/lib/executor.rb'); - // Validate the actual packed sources before warmup, outside timed operations. - vm.eval("require 'digest/sha2'"); - for (const [path, expected] of Object.entries(metadata.source_hashes)) { - const actual = vm.eval(`Digest::SHA256.file(${JSON.stringify(`/lib/${path}`)}).hexdigest`).toString(); - assert.equal(actual, expected, `Packed source differs: ${path}`); - } - - // Match the unthrottled worker's core, VFS copies, and transferable messages. - // The audio and final video are retained for checksums after the timer stops. - function step(retainAudio) { - const frames = core.runFrame(CONDITIONS.direction_mask, CONDITIONS.action_mask); - const audio = core.popAudio(); - let receivedAudio = null; - if (audio && audio.byteLength > 0) { - receivedAudio = structuredClone({ type: 'audioData', data: audio }, { transfer: [audio] }).data; - } - const video = core.framebuffer(); - const receivedVideo = structuredClone({ type: 'pixelData', data: video, frameCount: frames }, { transfer: [video] }).data; - if (retainAudio && receivedAudio) measuredAudio.push(receivedAudio); - return { frames, video: receivedVideo }; - } - - const measuredAudio = []; - const warmupStart = performance.now(); - for (let i = 0; i < CONDITIONS.warmup_run_frame_calls; i++) step(false); - const warmupMs = performance.now() - warmupStart; - - let frames = 0; - let finalVideo; - const measureStart = performance.now(); - for (let i = 0; i < CONDITIONS.measured_run_frame_calls; i++) { - const output = step(true); - frames += output.frames; - finalVideo = output.video; - } - const elapsedMs = performance.now() - measureStart; - - assert.equal(frames, CONDITIONS.measured_run_frame_calls, 'The fixed fixture must produce exactly one frame per call'); - assert.equal(finalVideo.byteLength, 160 * 144 * 4); - const audioHash = createHash('sha256'); - let audioBytes = 0; - for (const data of measuredAudio) { - assert.equal(data.byteLength % 8, 0, 'Float32 stereo audio must contain complete sample pairs'); - audioHash.update(new Uint8Array(data)); - audioBytes += data.byteLength; - } - assert.ok(audioBytes > 0, 'The APU must remain enabled and produce audio'); - const stateHex = vm.eval('Marshal.dump($executor.instance_variable_get(:@emulator)).unpack1("H*")').toString(); - const stateBytes = Buffer.from(stateHex, 'hex'); - return { - pair, label, wasm_path: wasmPath, artifact_sha256: hash(wasmBytes), runtime, fixtures: metadata, - fresh_process_cold_compile_ms: compileMs, - fresh_vm_init_rom_ms: initMs, - warmup_ms: warmupMs, - measured_ms: elapsedMs, - measured_run_frame_calls: CONDITIONS.measured_run_frame_calls, - completed_frames: frames, - emulation_ms_per_frame: elapsedMs / frames, - emulation_fps: frames * 1000 / elapsedMs, - final_framebuffer_sha256: hash(finalVideo), - measured_audio_sha256: audioHash.digest('hex'), - measured_audio_bytes: audioBytes, - measured_audio_float32_values: audioBytes / 4, - measured_audio_stereo_samples: audioBytes / 8, - measured_audio_blocks: measuredAudio.length, - final_emulator_marshal_state_sha256: hash(stateBytes), - final_emulator_marshal_state_bytes: stateBytes.length, - }; -} - -function runTrial(label, pair, wasmPath) { - const child = spawnSync(process.execPath, [SCRIPT, '--internal-trial', label, String(pair), wasmPath], { - encoding: 'utf8', maxBuffer: 4 * 1024 * 1024, - env: { ...process.env, RUBYOPT: '', RUBY_YJIT_ENABLE: '', RUBY_ZJIT_ENABLE: '' }, - }); - if (child.status !== 0) throw new Error(`${label} pair ${pair} failed: ${child.error?.message || child.stderr || child.stdout}`); - const line = child.stdout.split('\n').find(value => value.startsWith(TRIAL_MARKER)); - assert.ok(line, `Missing trial JSON for ${label} pair ${pair}`); - const result = JSON.parse(line.slice(TRIAL_MARKER.length)); - if (child.stderr.trim()) process.stderr.write(child.stderr); - return result; -} - -function summarize(results) { - const baseline = results.filter(result => result.label === 'baseline'); - const candidate = results.filter(result => result.label === 'candidate'); - const equalFields = ['completed_frames', 'final_framebuffer_sha256', 'measured_audio_sha256', 'measured_audio_bytes', 'measured_audio_blocks', 'final_emulator_marshal_state_sha256', 'final_emulator_marshal_state_bytes']; - const checksumMismatches = []; - for (const result of results) { - for (const field of equalFields) { - if (result[field] !== results[0][field]) checksumMismatches.push({ pair: result.pair, label: result.label, field, expected: results[0][field], actual: result[field] }); - } - } - const pairs = Array.from({ length: CONDITIONS.paired_trials }, (_, index) => { - const pair = index + 1; - const b = baseline.find(result => result.pair === pair); - const c = candidate.find(result => result.pair === pair); - return { - pair, order: CONDITIONS.orders[index], - baseline_ms: b.measured_ms, candidate_ms: c.measured_ms, - baseline_fps: b.emulation_fps, candidate_fps: c.emulation_fps, - candidate_faster: c.measured_ms < b.measured_ms, - fps_speedup_percent: (b.measured_ms / c.measured_ms - 1) * 100, - time_reduction_percent: (1 - c.measured_ms / b.measured_ms) * 100, - }; - }); - const allPairsFaster = pairs.every(pair => pair.candidate_faster); - const medianPairedSpeedupPercent = median(pairs.map(pair => pair.fps_speedup_percent)); - const checksumsEqual = checksumMismatches.length === 0; - const accepted = checksumsEqual && allPairsFaster && medianPairedSpeedupPercent >= CONDITIONS.min_median_paired_speedup_percent; - return { - pairs, checksums_equal: checksumsEqual, checksum_mismatches: checksumMismatches, - all_five_pairs_candidate_faster: allPairsFaster, - median_paired_fps_speedup_percent: medianPairedSpeedupPercent, - baseline_median_ms: median(baseline.map(result => result.measured_ms)), - candidate_median_ms: median(candidate.map(result => result.measured_ms)), - baseline_median_fps: median(baseline.map(result => result.emulation_fps)), - candidate_median_fps: median(candidate.map(result => result.emulation_fps)), - accept_candidate: accepted, - selection: accepted ? 'candidate' : 'baseline', - }; -} - -if (process.argv[2] === '--internal-trial') { - const [, , , label, pair, wasmPath] = process.argv; - try { - const result = await trial(label, Number(pair), wasmPath); - console.log(TRIAL_MARKER + JSON.stringify(result)); - } catch (error) { - console.error(error.stack || String(error)); - process.exitCode = 1; - } -} else { - const [baselineArg, candidateArg, jsonArg] = process.argv.slice(2); - if (!baselineArg || !candidateArg || process.argv.length > 5) { - console.error(`Usage: node ${SCRIPT} BASELINE.wasm CANDIDATE.wasm [OUTPUT.json]`); - process.exitCode = 2; - } else { - const baselinePath = resolve(baselineArg); - const candidatePath = resolve(candidateArg); - const outputPath = jsonArg ? resolve(jsonArg) : null; - const initialFixtures = fixtures().metadata; - const artifacts = { baseline: { path: baselinePath, sha256: hash(readFileSync(baselinePath)) }, candidate: { path: candidatePath, sha256: hash(readFileSync(candidatePath)) } }; - assert.notEqual(artifacts.baseline.sha256, artifacts.candidate.sha256, 'Candidate and baseline artifacts must differ'); - const report = { - schema_version: 2, created_at: new Date().toISOString(), conditions: CONDITIONS, - environment: { node: process.version, versions: process.versions, platform: platform(), arch: arch(), os_release: release(), cpu: cpus()[0]?.model, logical_cpus: cpus().length }, - fixtures: initialFixtures, artifacts, trials: [], - }; - try { - for (let index = 0; index < CONDITIONS.paired_trials; index++) { - for (const label of CONDITIONS.orders[index]) { - const result = runTrial(label, index + 1, artifacts[label].path); - assert.equal(result.artifact_sha256, artifacts[label].sha256, 'Artifact changed during the comparison'); - assert.deepEqual(result.fixtures, initialFixtures, 'Fixture or SDK changed during the comparison'); - report.trials.push(result); - console.error(`pair ${index + 1} ${label}: ${result.measured_ms.toFixed(3)} ms, ${result.emulation_fps.toFixed(3)} FPS`); - } - } - assert.deepEqual(fixtures().metadata, initialFixtures, 'Fixtures changed during the comparison'); - for (const artifact of Object.values(artifacts)) assert.equal(hash(readFileSync(artifact.path)), artifact.sha256); - report.summary = summarize(report.trials); - } catch (error) { - report.error = error.stack || String(error); - report.summary = { accept_candidate: false, selection: 'baseline', reason: 'Comparison incomplete or fixtures changed; do not claim a speedup.' }; - process.exitCode = 1; - } - const json = JSON.stringify(report, null, 2) + '\n'; - if (outputPath) writeFileSync(outputPath, json); - process.stdout.write(json); - } -} diff --git a/scripts/test_wasm_frontend.cjs b/scripts/test_wasm_frontend.cjs deleted file mode 100644 index 18ce4ab..0000000 --- a/scripts/test_wasm_frontend.cjs +++ /dev/null @@ -1,375 +0,0 @@ -// Deterministic contracts for the browser layer; real Ruby/Wasm tests are separate. -// Run: node scripts/test_wasm_frontend.cjs -const assert = require('node:assert/strict'); -const fs = require('node:fs'); -const path = require('node:path'); -const vm = require('node:vm'); -const docs = path.resolve(__dirname, '../docs'); - -const FRAME_MS = 70224 / 4194304 * 1000; - -function loopHarness(EmulationLoop, frameResult = 1, {frameMs = 0, timerTurnaround = 0} = {}) { - let time = 0; - let nextTimer = 0; - let audioDrains = 0; - const timers = new Map(); - const calls = []; - const messages = []; - const frames = []; - const adapter = { - runFrame(direction, action) { - calls.push({direction, action, startedAt: time}); - time += frameMs; - return frameResult; - }, - runCycles() { throw new Error('Presentation throttling must not split frames into cycle batches'); }, - framebuffer() { const buffer = new ArrayBuffer(160 * 144 * 4); frames.push(buffer); return buffer; }, - popAudio() { audioDrains++; return new Float32Array([.25, -.25]).buffer; }, - }; - const loop = new EmulationLoop(adapter, (message, transfers) => messages.push({message, transfers}), { - now: () => time, - setTimeout: (callback, delay) => { timers.set(++nextTimer, {callback, delay}); return nextTimer; }, - clearTimeout: handle => timers.delete(handle), - }); - function advance(now) { - time = now; - const [handle, task] = timers.entries().next().value; - timers.delete(handle); - task.callback(); - } - return { - loop, adapter, calls, messages, frames, timers, - drains: () => audioDrains, - now: () => time, - nextDelay: () => timers.values().next().value.delay, - advance, - runNext() { advance(time + Math.max(timerTurnaround, timers.values().next().value.delay)); }, - }; -} - -function testClock(EmulationLoop) { - const fast = loopHarness(EmulationLoop, 1, {frameMs: 2}); - fast.loop.start(); - fast.loop.start(); - assert.equal(fast.timers.size, 1, 'start is idempotent'); - assert.equal(fast.calls.length, 1, 'the first frame is presented immediately'); - assert.equal(fast.nextDelay(), Math.ceil(FRAME_MS - 2), 'execution time is included in the frame interval'); - fast.advance(10); - assert.equal(fast.calls.length, 1, 'an early timer must not run Ruby'); - assert.equal(fast.drains(), 1); - fast.advance(FRAME_MS); - assert.equal(fast.calls.length, 2); - for (let i = 0; i < 240; i++) fast.runNext(); - const rate = (fast.calls.length - 1) * 1000 / (fast.calls.at(-1).startedAt - fast.calls[0].startedAt); - assert.ok(rate > 59 && rate <= 60, `fast adapters are capped near the Game Boy frame rate: ${rate}`); - const fastPixels = fast.messages.filter(item => item.message.type === 'pixelData'); - assert.equal(fastPixels.length, fast.calls.length); - assert.ok(fastPixels.every(item => item.message.frameCount === 1), 'every calculated frame is presented'); - assert.equal(fast.messages.filter(item => item.message.type === 'audioData').length, 0, 'mute is the default'); - assert.equal(fast.drains(), fast.calls.length, 'audio is drained once per frame'); - fast.loop.stop(); - assert.equal(fast.timers.size, 0, 'stop cancels the loop'); - - const paused = loopHarness(EmulationLoop, 1, {frameMs: 2}); - paused.loop.start(); - paused.advance(1000); - assert.equal(paused.calls.length, 2, 'a one-second timer pause runs one frame, without catching up'); - assert.equal(paused.nextDelay(), Math.ceil(FRAME_MS - 2), 'pause recovery resumes ordinary pacing'); - paused.loop.stop(); - paused.loop.start(); - assert.equal(paused.calls.length, 3, 'restart resets the deadline instead of replaying elapsed time'); - paused.loop.stop(); - - const slow = loopHarness(EmulationLoop, 1, {frameMs: 28, timerTurnaround: 4}); - const unlimited = loopHarness(EmulationLoop, 1, {frameMs: 28, timerTurnaround: 4}); - unlimited.loop.setThrottle(false); - slow.loop.start(); - unlimited.loop.start(); - for (let i = 0; i < 100; i++) { - assert.equal(slow.nextDelay(), 0, 'a frame slower than the limit incurs no extra wait'); - const before = slow.calls.length; - slow.runNext(); - unlimited.runNext(); - assert.equal(slow.calls.length, before + 1, 'slow hosts yield after every frame'); - } - assert.equal(slow.calls.length, unlimited.calls.length); - assert.equal(slow.now(), unlimited.now(), 'limited slow hosts have the same throughput as unlimited hosts'); - assert.equal(slow.messages.filter(item => item.message.type === 'pixelData').length, slow.calls.length); - const packet = unlimited.messages.find(item => item.message.type === 'pixelData'); - assert.equal(packet.message.data, unlimited.frames[0]); - assert.equal(packet.transfers[0], packet.message.data); - assert.equal(packet.message.frameCount, 1); - assert.equal(packet.message.completedFrames, 1); - slow.loop.stop(); - unlimited.loop.stop(); - - const toggled = loopHarness(EmulationLoop); - toggled.loop.setMuted(false); - toggled.loop.start(); - toggled.loop.setThrottle(false); - toggled.advance(1); - assert.equal(toggled.calls.length, 2, 'switching off the limit permits the next task'); - assert.equal(toggled.nextDelay(), 0); - toggled.loop.setThrottle(true); - toggled.advance(2); - assert.equal(toggled.calls.length, 3, 'switching on resets the deadline'); - assert.equal(toggled.messages.filter(item => item.message.type === 'audioData').length, 3); - toggled.advance(3); - assert.equal(toggled.calls.length, 3, 'subsequent limited tasks respect the new deadline'); - toggled.loop.stop(); - - const lcdOff = loopHarness(EmulationLoop, 0); - lcdOff.loop.updateInput('KeyI', true); - lcdOff.loop.updateInput('KeyI', false); - lcdOff.loop.start(); - lcdOff.runNext(); - assert.equal(lcdOff.calls[0].action, 7); - assert.equal(lcdOff.calls[1].action, 15, 'LCD-off fallback clears the one-frame input latch'); - assert.equal(lcdOff.messages.filter(item => item.message.type === 'pixelData').length, 2); - assert.equal(lcdOff.messages.find(item => item.message.type === 'pixelData').message.frameCount, 0); - lcdOff.loop.stop(); - - const failed = loopHarness(EmulationLoop); - failed.adapter.runFrame = () => { throw new Error('emulator failed'); }; - failed.loop.start(); - assert.equal(failed.loop.running, false); - assert.equal(failed.timers.size, 0); - assert.equal(failed.messages[0].message.message, 'emulator failed'); -} - -function testInput(EmulationLoop) { - const shortPress = loopHarness(EmulationLoop); - shortPress.loop.start(); - shortPress.loop.updateInput('KeyI', true); - shortPress.loop.updateInput('KeyI', false); - shortPress.advance(10); - assert.equal(shortPress.calls.length, 1, 'a short press survives waiting for the next frame'); - shortPress.runNext(); - assert.equal(shortPress.calls[1].action, 7); - shortPress.runNext(); - assert.equal(shortPress.calls[2].action, 15, 'the latch clears after the next frame'); - shortPress.loop.updateInput('KeyK', true); - assert.equal(shortPress.loop.inputMasks()[1], 14, 'K remains Game Boy A'); - shortPress.loop.updateInput('KeyJ', true); - assert.equal(shortPress.loop.inputMasks()[1], 12, 'J remains Game Boy B'); - shortPress.loop.releaseInputs(); - assert.deepEqual(shortPress.loop.inputMasks(), [15, 15]); - shortPress.loop.stop(); -} - -function testWorklet() { - let Processor; - const context = vm.createContext({ - Float32Array, Math, sampleRate: 48000, - AudioWorkletProcessor: class { constructor() { this.port = {}; } }, - registerProcessor: (_name, value) => { Processor = value; }, - }); - vm.runInContext(fs.readFileSync(path.join(docs, 'audio-worklet.js'), 'utf8'), context); - const processor = new Processor(); - const samples = new Float32Array(1024); - for (let i = 0; i < samples.length; i += 2) { samples[i] = .25; samples[i + 1] = -.25; } - processor.port.onmessage({ data: samples.buffer }); - const outputs = [[new Float32Array(128), new Float32Array(128)]]; - processor.process([], outputs); - assert.equal(outputs[0][0][0], .25); - assert.equal(outputs[0][1][127], -.25, 'stereo channel order is preserved'); - processor.port.onmessage({ data: 'clear' }); - processor.process([], outputs); - assert.ok(outputs[0].every(channel => channel.every(value => value === 0)), 'cleared/empty audio produces silence'); - processor.port.onmessage({ data: new Float32Array(48000).buffer }); - assert.equal(processor.writeCursor - processor.readCursor, 2047, '500ms burst is bounded below 43ms'); - context.sampleRate = 44100; - const resampled = new Processor(); - resampled.port.onmessage({ data: samples.buffer }); - resampled.process([], outputs); - assert.equal(resampled.readCursor, 139, '44.1kHz output resamples 48kHz source duration'); -} - -function indexHarness(search = '', {workletLoad = Promise.resolve(), documentUrl = 'http://example.test/rubyboy/'} = {}) { - const messages = []; - const documentListeners = {}; - const windowListeners = {}; - const readers = []; - const audioContexts = []; - const worklets = []; - const elements = new Map(); - for (const id of ['canvas', 'rom-input', 'rom-select-box', 'fps-display', 'rom-upload-button', 'throttle-toggle', 'mute-toggle']) { - elements.set(id, { width: 320, height: 288, listeners: {}, files: [], classList: {remove() {}}, - addEventListener(type, callback) { this.listeners[type] = callback; } }); - } - for (const id of ['throttle-toggle', 'mute-toggle']) { - Object.assign(elements.get(id), {tagName: 'INPUT', type: 'checkbox', checked: true}); - } - const canvasContext = {scale() {}, fillRect() {}, putImageData() {}, drawImage() {}}; - elements.get('canvas').getContext = () => canvasContext; - const buttons = ['KeyK', 'KeyJ', 'KeyI', 'KeyU'].map(code => ({ dataset: {code}, listeners: {}, setPointerCapture() {}, - addEventListener(type, callback) { this.listeners[type] = callback; } })); - const worker = {postMessage: message => messages.push(message)}; - class AudioContext { - constructor(options) { - this.state = 'suspended'; this.options = options; this.resumes = 0; this.suspends = 0; this.modules = []; - this.audioWorklet = {addModule: url => { this.modules.push(String(url)); return workletLoad; }}; audioContexts.push(this); - } - resume() { this.resumes++; this.state = 'running'; return Promise.resolve(); } - suspend() { this.suspends++; this.state = 'suspended'; return Promise.resolve(); } - } - class AudioWorkletNode { - constructor() { this.messages = []; this.port = {postMessage: message => this.messages.push(message)}; worklets.push(this); } - connect() {} - } - const context = vm.createContext({ - Worker: function(url, options) { worker.url = new URL(url, documentUrl).href; worker.options = {...options}; return worker; }, - URL, URLSearchParams, Map, Promise, Uint8ClampedArray, AudioWorkletNode, - window: { location: {search}, AudioContext, addEventListener(type, callback) { windowListeners[type] = callback; } }, - document: { - currentScript: {src: 'http://example.test/rubyboy/index.js'}, - getElementById: id => elements.get(id), querySelectorAll: () => buttons, - createElement: () => ({getContext: () => canvasContext}), - addEventListener(type, callback) { (documentListeners[type] ||= []).push(callback); }, - }, - FileReader: class { constructor() { readers.push(this); } readAsArrayBuffer() {} }, - ImageData: class {}, performance: {now: () => 0}, console, - }); - vm.runInContext(fs.readFileSync(path.join(docs, 'index.js'), 'utf8'), context); - context.document.currentScript = null; - function dispatch(type, event) { for (const listener of documentListeners[type] || []) listener(event); } - function toggle(id, checked) { - const element = elements.get(id); - element.checked = checked; - element.listeners.change({target: element}); - } - return {messages, worker, elements, buttons, readers, audioContexts, worklets, dispatch, toggle, windowListeners}; -} - -async function testIndex() { - for (const documentUrl of ['http://example.test/rubyboy/', 'http://example.test/rubyboy/spinel/']) { - const reused = indexHarness('', {documentUrl}); - assert.equal(reused.worker.url, new URL('worker.js', documentUrl).href, 'the page selects its own backend worker'); - assert.deepEqual(reused.worker.options, {type: 'module'}); - reused.toggle('mute-toggle', false); - await new Promise(setImmediate); - assert.deepEqual(reused.audioContexts[0].modules, ['http://example.test/rubyboy/audio-worklet.js'], - 'both pages load the shared worklet after currentScript becomes null'); - } - const normal = indexHarness(); - assert.equal(normal.elements.get('throttle-toggle').checked, true, 'throttle checkbox defaults to checked'); - assert.equal(normal.elements.get('mute-toggle').checked, true, 'mute checkbox defaults to checked'); - normal.worker.onmessage({data: {type: 'initialized'}}); - assert.equal(normal.messages.find(message => message.type === 'setThrottle').enabled, true); - assert.equal(normal.messages.find(message => message.type === 'setMute').enabled, true); - const event = code => ({code, target: {tagName: 'BODY'}, preventDefault() {}}); - normal.dispatch('keydown', event('KeyK')); - assert.equal(normal.messages.at(-1).action, 1); - const start = normal.buttons.find(button => button.dataset.code === 'KeyI'); - start.listeners.pointerdown({pointerId: 1, preventDefault() {}}); - assert.equal(normal.messages.at(-1).action, 9, 'keyboard and touch/pointer sources combine'); - normal.dispatch('keyup', event('KeyK')); - assert.equal(normal.messages.at(-1).action, 8, 'keyboard release keeps a held pointer button'); - start.listeners.pointercancel({pointerId: 1}); - assert.equal(normal.messages.at(-1).action, 0); - const beforeClick = normal.messages.length; - start.listeners.click({detail: 0}); - assert.equal(normal.messages[beforeClick].action, 8); - assert.equal(normal.messages[beforeClick + 1].action, 0, 'accessibility/keyboard button click pulses input'); - const beforePointerClick = normal.messages.length; - start.listeners.click({detail: 1}); - assert.equal(normal.messages.length, beforePointerClick, 'pointer click is not duplicated'); - normal.windowListeners.blur(); - assert.equal(normal.messages.at(-1).type, 'releaseInputs'); - assert.equal(normal.audioContexts.length, 0, 'muted playback does not open an audio device'); - let checkboxDefaultPrevented = false; - const beforeCheckboxKey = normal.messages.length; - normal.dispatch('keydown', {code: 'Space', target: normal.elements.get('mute-toggle'), - preventDefault() { checkboxDefaultPrevented = true; }}); - assert.equal(checkboxDefaultPrevented, false, 'Space can activate a focused checkbox'); - assert.equal(normal.messages.length, beforeCheckboxKey, 'checkbox activation key is not Game Boy input'); - normal.toggle('throttle-toggle', false); - assert.equal(normal.messages.at(-1).type, 'setThrottle'); - assert.equal(normal.messages.at(-1).enabled, false, 'throttle changes reach the worker immediately'); - normal.toggle('throttle-toggle', true); - assert.equal(normal.messages.at(-1).enabled, true, 'throttle can be restored without restarting'); - normal.elements.get('rom-input').listeners.change({target: {files: [{}]}}); - normal.elements.get('rom-select-box').listeners.change({target: {value: 'bgbtest.gb'}}); - normal.readers[0].onload({target: {result: new ArrayBuffer(32768)}}); - assert.equal(normal.messages.filter(message => message.type === 'loadROM').length, 0, 'late uploaded file cannot replace a later ROM selection'); - const unlimited = indexHarness('?throttle=0&mute=1'); - assert.equal(unlimited.elements.get('throttle-toggle').checked, false, 'URL throttle override is visible in the checkbox'); - assert.equal(unlimited.elements.get('mute-toggle').checked, true); - unlimited.worker.onmessage({data: {type: 'initialized'}}); - assert.equal(unlimited.messages.find(message => message.type === 'setThrottle').enabled, false); - assert.equal(unlimited.messages.find(message => message.type === 'setMute').enabled, true); - const sound = indexHarness('?throttle=1&mute=0'); - assert.equal(sound.elements.get('throttle-toggle').checked, true); - assert.equal(sound.elements.get('mute-toggle').checked, false, 'URL mute override is visible in the checkbox'); - sound.worker.onmessage({data: {type: 'initialized'}}); - assert.equal(sound.audioContexts.length, 0, 'audio waits for a user gesture'); - sound.dispatch('keydown', event('KeyI')); - await new Promise(setImmediate); - assert.equal(sound.audioContexts[0].resumes, 1, 'first gesture resumes audio before worklet startup'); - const samples = new Float32Array([.25, -.25]).buffer; - sound.worker.onmessage({data: {type: 'audioData', data: samples}}); - assert.equal(sound.worklets[0].messages[0], samples); - sound.toggle('mute-toggle', true); - assert.equal(sound.messages.at(-1).type, 'setMute'); - assert.equal(sound.messages.at(-1).enabled, true, 'mute changes reach the worker immediately'); - await new Promise(setImmediate); - assert.equal(sound.worklets[0].messages.at(-1), 'clear', 'muting discards queued audio'); - assert.equal(sound.audioContexts[0].state, 'suspended', 'muting suspends the audio device'); - const messagesWhileMuted = sound.worklets[0].messages.length; - sound.worker.onmessage({data: {type: 'audioData', data: samples}}); - sound.dispatch('keydown', event('KeyK')); - await new Promise(setImmediate); - assert.equal(sound.worklets[0].messages.length, messagesWhileMuted, 'muted playback drops incoming audio'); - assert.equal(sound.audioContexts[0].resumes, 1, 'game input does not resume muted audio'); - sound.toggle('mute-toggle', false); - assert.equal(sound.messages.at(-1).type, 'setMute'); - assert.equal(sound.messages.at(-1).enabled, false); - await new Promise(setImmediate); - assert.equal(sound.audioContexts.length, 1, 'unmuting reuses the audio context'); - assert.equal(sound.worklets.length, 1, 'unmuting reuses the audio processor'); - assert.equal(sound.audioContexts[0].state, 'running', 'unmute gesture resumes the audio device'); - sound.worker.onmessage({data: {type: 'audioData', data: samples}}); - assert.equal(sound.worklets[0].messages.at(-1), samples, 'audio delivery resumes after unmuting'); - - const preinit = indexHarness(); - preinit.toggle('throttle-toggle', false); - preinit.toggle('mute-toggle', false); - assert.equal(preinit.messages.at(-1).type, 'setMute', 'toggle commands also send while the worker initializes'); - preinit.worker.onmessage({data: {type: 'initialized'}}); - assert.equal(preinit.messages.at(-3).type, 'setThrottle'); - assert.equal(preinit.messages.at(-3).enabled, false, 'initialization uses the current throttle checkbox'); - assert.equal(preinit.messages.at(-2).type, 'setMute'); - assert.equal(preinit.messages.at(-2).enabled, false, 'initialization uses the current mute checkbox'); - assert.equal(preinit.messages.at(-1).type, 'startRubyboy', 'current playback options precede starting the emulator'); - await new Promise(setImmediate); - - let finishWorklet; - const duringStartup = indexHarness('', {workletLoad: new Promise(resolve => { finishWorklet = resolve; })}); - duringStartup.toggle('mute-toggle', false); - assert.equal(duringStartup.audioContexts[0].resumes, 1, 'unmute resumes before the worklet download finishes'); - assert.equal(duringStartup.worklets.length, 0); - duringStartup.toggle('mute-toggle', true); - finishWorklet(); - await new Promise(setImmediate); - assert.equal(duringStartup.audioContexts[0].state, 'suspended', 're-muting during worklet startup stays muted'); - assert.equal(duringStartup.worklets[0].messages.at(-1), 'clear', 'a processor created after muting starts with its queue cleared'); - const messagesAfterStartup = duringStartup.worklets[0].messages.length; - duringStartup.worker.onmessage({data: {type: 'audioData', data: samples}}); - assert.equal(duringStartup.worklets[0].messages.length, messagesAfterStartup); - duringStartup.toggle('mute-toggle', false); - await new Promise(setImmediate); - assert.equal(duringStartup.audioContexts.length, 1); - assert.equal(duringStartup.worklets.length, 1); - duringStartup.worker.onmessage({data: {type: 'audioData', data: samples}}); - assert.equal(duringStartup.worklets[0].messages.at(-1), samples, 'audio can resume after muting during startup'); -} - -(async () => { - const source = fs.readFileSync(path.join(docs, 'emulation-loop.js'), 'utf8'); - const {EmulationLoop} = await import('data:text/javascript;base64,' + Buffer.from(source).toString('base64')); - testClock(EmulationLoop); - testInput(EmulationLoop); - testWorklet(); - await testIndex(); - console.log('Wasm frontend tests passed: frame pacing and slow-host throughput, normal/unlimited, input latch and sources, audio transfer/ring/resampling, live playback toggles and startup race, URL defaults and upload race.'); -})().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/scripts/test_wasm_runtime.mjs b/scripts/test_wasm_runtime.mjs deleted file mode 100644 index f521bea..0000000 --- a/scripts/test_wasm_runtime.mjs +++ /dev/null @@ -1,118 +0,0 @@ -// Run after the browser runtime dependencies and docs/rubyboy.wasm are available. -// mise exec ruby@4.0.7 -- node scripts/test_wasm_runtime.mjs [path/to/rubyboy.wasm] -import assert from 'node:assert/strict'; -import { createHash } from 'node:crypto'; -import { readFileSync } from 'node:fs'; -import { spawnSync } from 'node:child_process'; -import { fileURLToPath } from 'node:url'; -import { DefaultRubyVM } from '../build/browser-runtime/node_modules/@ruby/wasm-wasi/dist/esm/browser.js'; -import { File } from '../build/browser-runtime/node_modules/@bjorn3/browser_wasi_shim/dist/index.js'; -import { RubyboyVM } from '../docs/rubyboy-vm.js'; - -const sourceRoot = new URL('../', import.meta.url); -const hash = bytes => createHash('sha256').update(new Uint8Array(bytes)).digest('hex'); -const romBytes = readFileSync(new URL('lib/roms/tobu.gb', sourceRoot)); -const wasmPath = process.argv[2] || fileURLToPath(new URL('docs/rubyboy.wasm', sourceRoot)); -const wasmBytes = readFileSync(wasmPath); - -const referenceProcess = spawnSync('ruby', [fileURLToPath(new URL('wasm_reference.rb', import.meta.url))], { - cwd: fileURLToPath(sourceRoot), encoding: 'utf8', maxBuffer: 1024 * 1024, -}); -assert.equal(referenceProcess.status, 0, referenceProcess.stderr || 'CRuby reference failed'); -const reference = JSON.parse(referenceProcess.stdout); -assert.equal(hash(romBytes), reference.rom_sha256); - -console.log(`Host: ${reference.ruby}`); -console.log(`Wasm: ${wasmPath}`); -console.log(`Wasm SHA-256: ${hash(wasmBytes)}`); -console.log(`ROM SHA-256: ${reference.rom_sha256}`); - -const module = await WebAssembly.compile(wasmBytes); -const { vm, wasi } = await DefaultRubyVM(module); -const root = wasi.fds[3].dir; -const core = new RubyboyVM(vm, root, File); -console.log(`Browser runtime: ${vm.eval('RUBY_DESCRIPTION').toString()}`); -const bundledRom = Buffer.from(vm.eval("File.binread('/lib/roms/tobu.gb').unpack1('H*')").toString(), 'hex'); -assert.equal(hash(bundledRom), hash(romBytes), 'The default packed ROM must match the repository ROM'); -assert.equal(vm.eval('Executor.instance_method(:exec_cycles).source_location.first').toString(), - '/lib/executor.rb'); - -for (const scenario of reference.scenarios) { - // Upload the current repository ROM so the old packed asset cannot change the fixture. - core.loadUploadedRom(Uint8Array.from(romBytes).buffer); - assert.equal(root.contents.has('rom.data'), false); - assert.equal(root.contents.has('video.data'), false); - assert.equal(core.popAudio(), null); - const accumulatedAudio = []; - let frames = 0; - let audioBytes = 0; - for (let tick = 1; tick <= reference.ticks; tick++) { - const [direction, action] = reference.inputs[tick - 1]; - const audio = []; - if (scenario.mode === 'frame') { - frames += core.runFrame(direction, action); - const block = core.popAudio(); - if (block) audio.push(Buffer.from(block)); - } else { - for (const budget of reference.cycle_budgets) { - frames += core.runCycles(budget, direction, action); - const block = core.popAudio(); - if (block) audio.push(Buffer.from(block)); - } - } - const tickAudio = Buffer.concat(audio); - accumulatedAudio.push(tickAudio); - audioBytes += tickAudio.length; - assert.equal(core.popAudio(), null, 'An audio block must be consumed only once'); - const expected = scenario.checkpoints.find(checkpoint => checkpoint.tick === tick); - if (!expected) continue; - const videoFile = root.contents.get('video.data'); - assert.equal(videoFile?.data.length, 160 * 144 * 4); - const video = core.framebuffer(); - assert.equal(root.contents.has('video.data'), false); - const actual = { - tick, frames, video_sha256: hash(video), - audio_sha256: hash(tickAudio), audio_bytes: tickAudio.length, - accumulated_audio_sha256: hash(Buffer.concat(accumulatedAudio)), - accumulated_audio_bytes: audioBytes, - }; - assert.deepEqual(actual, expected, `${scenario.mode} checkpoint ${tick}`); - // The worker transfers this copy; transfer must leave the WASI source usable. - const transfer = structuredClone(video, { transfer: [video] }); - assert.equal(video.byteLength, 0); - assert.equal(hash(videoFile.data), hash(transfer)); - console.log(`PASS ${scenario.mode}: tick ${tick}, ${frames} frames, ${audioBytes} audio bytes`); - } - assert.ok(audioBytes > 0, 'The APU must produce stereo audio'); -} - -// Tiny budgets must account for instruction overshoot instead of executing once per tick. -core.loadUploadedRom(Uint8Array.from(romBytes).buffer); -const budgets = [1, 3, 7, 8192]; -for (const budget of budgets) core.runCycles(budget, 14, 13); -const splitState = vm.eval('Marshal.dump($executor.instance_variable_get(:@emulator)).bytes.join(",")').toString(); -core.loadUploadedRom(Uint8Array.from(romBytes).buffer); -core.runCycles(budgets.reduce((sum, budget) => sum + budget, 0), 14, 13); -assert.equal(vm.eval('Marshal.dump($executor.instance_variable_get(:@emulator)).bytes.join(",")').toString(), splitState); -assert.throws(() => core.runCycles(32769, 15, 15), /Invalid cycle budget/); -assert.throws(() => core.loadUploadedRom(new ArrayBuffer(5)), /ROM size/); -assert.throws(() => core.loadPreInstalledRom('../other.gb'), /Unknown bundled ROM/); - -// Disabling the LCD must not trap the browser worker in a frame wait. -core.loadUploadedRom(Uint8Array.from(romBytes).buffer); -vm.eval('$executor.instance_variable_get(:@emulator).instance_variable_get(:@ppu).write_byte(0xff40, 0)'); -assert.equal(core.runFrame(15, 15), 1); -assert.equal(core.framebuffer().byteLength, 160 * 144 * 4); -assert.ok(core.popAudio().byteLength > 0); -core.clearOutputs(); -assert.equal(core.popAudio(), null); -assert.equal(root.contents.has('video.data'), false); - -// Existing ROMs and Ruby sources are packed together under /lib. -core.loadPreInstalledRom('tobu.gb'); -core.runFrame(15, 15); -assert.equal(core.framebuffer().byteLength, 160 * 144 * 4); -core.loadPreInstalledRom('bgbtest.gb'); -core.runFrame(15, 15); -assert.equal(core.framebuffer().byteLength, 160 * 144 * 4); -console.log('PASS packed sources, audio consumption, buffer transfer, split budgets, LCD-off, and bundled ROMs'); diff --git a/scripts/verify_wasm_package.mjs b/scripts/verify_wasm_package.mjs deleted file mode 100644 index fd10ef2..0000000 --- a/scripts/verify_wasm_package.mjs +++ /dev/null @@ -1,62 +0,0 @@ -// Verify the Ruby sources and ROMs embedded in the Wasm package. -// node scripts/verify_wasm_package.mjs [WASM_PATH] [EXPECTED_RUBY_VERSION] -import assert from 'node:assert/strict'; -import { createHash } from 'node:crypto'; -import { lstatSync, readFileSync, readdirSync } from 'node:fs'; -import { join, relative, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { DefaultRubyVM } from '../build/browser-runtime/node_modules/@ruby/wasm-wasi/dist/esm/browser.js'; - -const repoRoot = fileURLToPath(new URL('../', import.meta.url)); -const libRoot = join(repoRoot, 'lib'); -const wasmPath = process.argv[2] ? resolve(process.argv[2]) : join(repoRoot, 'docs', 'rubyboy.wasm'); -const versionSource = readFileSync(join(libRoot, 'rubyboy', 'version.rb'), 'utf8'); -const expectedAppVersion = versionSource.match(/\bVERSION\s*=\s*['"]([^'"]+)['"]/)[1]; -const sha256 = bytes => createHash('sha256').update(bytes).digest('hex'); - -function regularFiles(directory) { - return readdirSync(directory).sort().flatMap(name => { - const path = join(directory, name); - const stat = lstatSync(path); - if (stat.isDirectory()) return regularFiles(path); - return stat.isFile() ? [path] : []; - }); -} - -const files = regularFiles(libRoot).map(path => { - const bytes = readFileSync(path); - return { path: relative(libRoot, path).split('\\').join('/'), size: bytes.length, sha256: sha256(bytes) }; -}); -assert.ok(files.length > 0, 'Repository lib must contain regular files'); -const wasmBytes = readFileSync(wasmPath); -const module = await WebAssembly.compile(wasmBytes); -const { vm } = await DefaultRubyVM(module); - -// Read the sources and ROMs packed into the Wasm. -const rubyDescription = vm.eval('RUBY_DESCRIPTION').toString(); -assert.equal(vm.eval('RUBY_VERSION').toString(), process.argv[3] || '4.0.7', 'Wasm Ruby version'); -vm.eval("require '/lib/rubyboy/version'"); -const appVersion = vm.eval('Rubyboy::VERSION').toString(); -assert.equal(appVersion, expectedAppVersion, 'Packed Rubyboy version'); -vm.eval("require 'digest/sha2'"); - -vm.eval("require 'json'"); -const packedFiles = JSON.parse(vm.eval( - "JSON.generate(Dir.glob('/lib/**/*', File::FNM_DOTMATCH).select { |path| File.file?(path) }.map { |path| path.delete_prefix('/lib/') }.sort)" -).toString()); -assert.deepEqual(packedFiles, files.map(file => file.path).sort(), 'Packed /lib file list differs from repository'); - -for (const file of files) { - const packedPath = JSON.stringify(`/lib/${file.path}`); - const packedSize = Number(vm.eval(`File.size(${packedPath})`).toString()); - assert.equal(packedSize, file.size, `Packed size differs: ${file.path}`); - const packedHash = vm.eval(`Digest::SHA256.file(${packedPath}).hexdigest`).toString(); - assert.equal(packedHash, file.sha256, `Packed SHA-256 differs: ${file.path}`); -} - -console.log(`Wasm: ${wasmPath}`); -console.log(`Wasm SHA-256: ${sha256(wasmBytes)}`); -console.log(`Browser runtime: ${rubyDescription}`); -console.log(`Packed Rubyboy version: ${appVersion}`); -console.log(`PASS packed /lib matches repository: ${files.length} regular files, ${files.filter(file => file.path.endsWith('.rb')).length} Ruby sources, ${files.filter(file => file.path.endsWith('.gb')).length} ROMs, ${files.reduce((sum, file) => sum + file.size, 0)} bytes`); -console.log(`lib manifest SHA-256: ${sha256(files.map(file => `${file.path}\t${file.size}\t${file.sha256}\n`).join(''))}`); diff --git a/spec/bench_spec.rb b/spec/bench_spec.rb index 3e937d6..626ef57 100644 --- a/spec/bench_spec.rb +++ b/spec/bench_spec.rb @@ -1,115 +1,28 @@ # frozen_string_literal: true require 'bench' -require 'json' require 'open3' require 'rbconfig' RSpec.describe Rubyboy::Bench do - let(:framebuffer) { [0x12345678] } - let(:emulator) { instance_double(Rubyboy::EmulatorHeadless, step: nil, framebuffer:) } - let(:lcd) { instance_double(Rubyboy::Lcd, draw: nil, window_should_close?: false, close_window: nil) } + let(:emulator) { instance_double(Rubyboy::EmulatorHeadless, step: nil) } before do allow(Rubyboy::EmulatorHeadless).to receive(:new).and_return(emulator) - allow(Rubyboy::Lcd).to receive(:new).and_return(lcd) - allow(Process).to receive(:clock_gettime).with(Process::CLOCK_MONOTONIC, :nanosecond).and_return(1_000_000_000, 2_000_000_000) end it 'starts each trial from a fresh emulator, warms up identically, and aggregates measured durations' do second_emulator = instance_double(Rubyboy::EmulatorHeadless, step: nil) allow(Rubyboy::EmulatorHeadless).to receive(:new).with('test.gb').and_return(emulator, second_emulator) allow(Process).to receive(:clock_gettime).with(Process::CLOCK_MONOTONIC, :nanosecond).and_return(1_000_000_000, 2_000_000_000, 3_000_000_000, 5_000_000_000) - result = nil expect do - result = described_class.new.run(count: 2, frames: 3, warmup_frames: 2, rom_path: 'test.gb') + described_class.new.run(count: 2, frames: 3, warmup_frames: 2, rom_path: 'test.gb') end.to output("1: 1.0 sec\n2: 2.0 sec\nFPS: 2.0\n").to_stdout expect(Rubyboy::EmulatorHeadless).to have_received(:new).with('test.gb').twice expect(emulator).to have_received(:step).exactly(5).times expect(second_emulator).to have_received(:step).exactly(5).times - expect(result[:trials]).to eq([ - { trial: 1, frames: 3, elapsed_seconds: 1.0, fps: 3.0 }, - { trial: 2, frames: 3, elapsed_seconds: 2.0, fps: 1.5 } - ]) - expect(result[:total_frames]).to eq(6) - expect(result[:elapsed_seconds]).to eq(3.0) - expect(result[:fps]).to eq(2.0) - end - - it 'keeps headless trials free of display and audio device output' do - expect(Rubyboy::Lcd).not_to receive(:new) - expect(Rubyboy::Audio).not_to receive(:new) - - expect { described_class.new.run(count: 1, frames: 2) }.to output("1: 1.0 sec\nFPS: 2.0\n").to_stdout - - expect(emulator).to have_received(:step).twice - end - - it 'draws warmup and measured frames from the same headless core and excludes setup and cleanup from timing' do - events = [] - allow(Rubyboy::EmulatorHeadless).to receive(:new) { - events << :initialize - emulator - } - allow(Rubyboy::Lcd).to receive(:new) { - events << :open - lcd - } - allow(emulator).to receive(:step) { events << :step } - allow(lcd).to receive(:draw).with(framebuffer) { events << :draw } - allow(lcd).to receive(:window_should_close?) { - events << :poll - false - } - allow(lcd).to receive(:close_window) { events << :close } - times = [1_000_000_000, 2_000_000_000] - allow(Process).to receive(:clock_gettime).with(Process::CLOCK_MONOTONIC, :nanosecond) do - events << :clock - times.shift - end - - expect { described_class.new.run(count: 1, frames: 2, warmup_frames: 1, render: true) }.to output("1: 1.0 sec\nFPS: 2.0\n").to_stdout - - expect(events).to eq(%i[initialize open poll step draw clock poll step draw poll step draw clock close]) - expect(Rubyboy::Lcd).to have_received(:new).with(vsync: false).once - expect(lcd).to have_received(:draw).with(framebuffer).exactly(3).times - expect(lcd).to have_received(:close_window).once - end - - it 'closes the display and aborts without a completed-trial result when the window closes early' do - allow(lcd).to receive(:window_should_close?).and_return(false, true) - expect($stdout).not_to receive(:puts) - - expect do - described_class.new.run(count: 1, frames: 3, render: true) - end.to raise_error(Rubyboy::Bench::WindowClosed, 'Benchmark stopped because the window was closed.') - - expect(emulator).to have_received(:step).once - expect(lcd).to have_received(:close_window).once - end - - it 'also aborts and cleans up when the window closes during warmup' do - allow(lcd).to receive(:window_should_close?).and_return(true) - expect(Process).not_to receive(:clock_gettime) - - expect do - described_class.new.run(count: 1, frames: 3, warmup_frames: 1, render: true) - end.to raise_error(Rubyboy::Bench::WindowClosed) - - expect(emulator).not_to have_received(:step) - expect(lcd).to have_received(:close_window).once - end - - it 'closes the display when emulation raises an error' do - allow(emulator).to receive(:step).and_raise('emulation failed') - - expect do - described_class.new.run(count: 1, frames: 1, render: true) - end.to raise_error(RuntimeError, 'emulation failed') - - expect(lcd).to have_received(:close_window).once end %i[count frames].each do |option| @@ -133,54 +46,30 @@ end.to raise_error(ArgumentError, 'warmup_frames must be a nonnegative integer') end end - - it 'outputs one JSON document with conditions, actual runtime identity, and individual trial results' do - output = StringIO.new - allow($stdout).to receive(:puts) { |text| output.puts(text) } - - result = described_class.new.run(count: 1, frames: 2, warmup_frames: 1, rom_path: 'test.gb', json: true) - document = JSON.parse(output.string) - - expect(document).to eq(JSON.parse(JSON.generate(result))) - expect(document['rom_path']).to eq(File.expand_path('test.gb')) - expect(document['warmup_frames']).to eq(1) - expect(document['conditions']).to eq('apu' => true, 'audio_output' => false, 'realtime_sync' => false, 'vsync' => nil, 'input' => false, 'save' => false) - expect(document['runtime']['description']).to eq(RUBY_DESCRIPTION) - expect(document['runtime']['engine']).to eq(RUBY_ENGINE) - expect(document['runtime']['yjit_enabled']).to eq(defined?(RubyVM::YJIT) ? RubyVM::YJIT.enabled? : false) - expect(document['trials'].length).to eq(1) - expect(document['fps']).to eq(2.0) - end end RSpec.describe 'rubyboy-bench executable' do let(:repository_root) { File.expand_path('..', __dir__) } - it 'emits standalone JSON with the requested measured and warmup frame counts' do - stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-Ilib', 'exe/rubyboy-bench', '--count', '1', '--frames', '1', '--warmup-frames', '1', '--json', chdir: repository_root) + it 'runs the requested measured and warmup frames' do + stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-Ilib', 'exe/rubyboy-bench', '--count', '1', '--frames', '1', '--warmup-frames', '1', chdir: repository_root) expect(status.success?).to be(true), stderr - document = JSON.parse(stdout) - expect(document['frames']).to eq(1) - expect(document['warmup_frames']).to eq(1) - expect(document['total_frames']).to eq(1) - expect(document['trials'].length).to eq(1) - expect(document['render']).to be(false) - expect(document['fps']).to be > 0 + expect(stdout).to match(/^1: .* sec$/) + expect(stdout).to match(/^FPS: /) end - it 'rejects an invalid count with a failing exit status and no JSON output' do - stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-Ilib', 'exe/rubyboy-bench', '--count', '0', '--json', chdir: repository_root) + it 'rejects an invalid count with a failing exit status' do + _stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-Ilib', 'exe/rubyboy-bench', '--count', '0', chdir: repository_root) expect(status.exitstatus).to eq(1) - expect(stdout).to eq('') expect(stderr).to include('count must be a positive integer') end %w[--count --frames --warmup-frames].each do |option| %w[abc 1.5].each do |value| - it "rejects malformed #{option} #{value} without JSON or a stack trace" do - stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-Ilib', 'exe/rubyboy-bench', option, value, '--json', chdir: repository_root) + it "rejects malformed #{option} #{value} without a stack trace" do + stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-Ilib', 'exe/rubyboy-bench', option, value, chdir: repository_root) expect(status.exitstatus).to eq(1) expect(stdout).to eq('') @@ -191,10 +80,10 @@ end it 'rejects a negative warmup count instead of starting a trial' do - stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-Ilib', 'exe/rubyboy-bench', '--warmup-frames', '-1', '--json', chdir: repository_root) + stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-Ilib', 'exe/rubyboy-bench', '--warmup-frames', '-1', chdir: repository_root) expect(status.exitstatus).to eq(1) - expect(stdout).to eq('') + expect(stdout).not_to include('FPS') expect(stderr).to include('warmup_frames must be a nonnegative integer') end end diff --git a/spec/rubyboy/audio_spec.rb b/spec/rubyboy/audio_spec.rb index 37ecb88..c556dc2 100644 --- a/spec/rubyboy/audio_spec.rb +++ b/spec/rubyboy/audio_spec.rb @@ -1,6 +1,31 @@ # frozen_string_literal: true RSpec.describe Rubyboy::Audio do + describe '#queue' do + let(:audio) { described_class.allocate.tap { _1.instance_variable_set(:@device, 42) } } + + before { allow(Rubyboy::SDL).to receive(:QueueAudio) } + + it 'waits for a full queue to drain by default' do + allow(Rubyboy::SDL).to receive(:GetQueuedAudioSize).with(42).and_return(8193, 0) + allow(audio).to receive(:sleep) + + audio.queue([0.5, -0.5]) + + expect(audio).to have_received(:sleep).once + expect(Rubyboy::SDL).to have_received(:QueueAudio).once + end + + it 'drops the buffer instead of waiting when wait is false' do + allow(Rubyboy::SDL).to receive(:GetQueuedAudioSize).with(42).and_return(8193) + expect(audio).not_to receive(:sleep) + + audio.queue([0.5, -0.5], wait: false) + + expect(Rubyboy::SDL).not_to have_received(:QueueAudio) + end + end + describe '#clear_queue' do it 'clears queued audio for its SDL device' do audio = described_class.allocate diff --git a/spec/rubyboy/emulator_spec.rb b/spec/rubyboy/emulator_spec.rb index 8e0ccc6..640335e 100644 --- a/spec/rubyboy/emulator_spec.rb +++ b/spec/rubyboy/emulator_spec.rb @@ -6,8 +6,8 @@ it 'converts the frame count and forwards playback options' do emulator = instance_double(described_class, start: nil) - stub_const('ARGV', %w[--frames 3 --unlimited --no-audio game.gb]) - expect(described_class).to receive(:new).with('game.gb', audio: false).and_return(emulator) + stub_const('ARGV', %w[--frames 3 --unlimited game.gb]) + expect(described_class).to receive(:new).with('game.gb').and_return(emulator) load executable @@ -25,24 +25,6 @@ end end - describe '#initialize' do - it 'does not open an audio device when audio output is disabled' do - rom_path = File.expand_path('../../lib/roms/hello-world.gb', __dir__) - allow(Rubyboy::Lcd).to receive(:new).and_return(instance_double(Rubyboy::Lcd)) - expect(Rubyboy::Audio).not_to receive(:new) - - described_class.new(rom_path, audio: false) - end - - it 'opens an audio device by default' do - rom_path = File.expand_path('../../lib/roms/hello-world.gb', __dir__) - allow(Rubyboy::Lcd).to receive(:new).and_return(instance_double(Rubyboy::Lcd)) - expect(Rubyboy::Audio).to receive(:new).and_return(instance_double(Rubyboy::Audio)) - - described_class.new(rom_path) - end - end - describe '#start' do let(:emulator) { described_class.allocate } let(:cpu) { instance_double(Rubyboy::Cpu, exec: 4) } @@ -79,21 +61,10 @@ expect(emulator).to have_received(:save_save_file).once end - it 'continues synthesizing audio when audio output is disabled' do - emulator.instance_variable_set(:@audio, nil) - - emulator.start(frames: 2, realtime: false) - - expect(apu).to have_received(:step).with(4).twice - expect(apu).not_to have_received(:samples) - expect(audio).not_to have_received(:queue) - expect(lcd).to have_received(:draw).twice - end - - it 'queues synthesized audio when audio output is enabled' do + it 'queues audio without waiting for the device during unlimited playback' do emulator.start(frames: 2, realtime: false) - expect(audio).to have_received(:queue).with([0.25, -0.25]).twice + expect(audio).to have_received(:queue).with([0.25, -0.25], wait: false).twice end it 'synchronizes to real time by default and can stop after a finite frame count' do @@ -104,6 +75,7 @@ expect(Process).to have_received(:clock_gettime).exactly(3).times expect(cpu).to have_received(:exec).twice expect(lcd).to have_received(:draw).twice + expect(audio).to have_received(:queue).with([0.25, -0.25], wait: true).twice end it 'continues until the window closes when no frame count is specified' do @@ -174,14 +146,5 @@ expect(audio).to have_received(:clear_queue) end - - it 'can load a state when audio output is disabled' do - emulator = described_class.allocate - emulator.instance_variable_set(:@rom, instance_double(Rubyboy::Rom)) - emulator.instance_variable_set(:@audio, nil) - allow(Rubyboy::StateFile).to receive(:read).and_return(true) - - expect(emulator.load_state(path: 'game.state1')).to be true - end end end diff --git a/spec/rubyboy/emulator_wasm_spec.rb b/spec/rubyboy/emulator_wasm_spec.rb index 935f376..9d82192 100644 --- a/spec/rubyboy/emulator_wasm_spec.rb +++ b/spec/rubyboy/emulator_wasm_spec.rb @@ -25,50 +25,43 @@ def loop_rom it 'matches the desktop core, including APU state and copied audio blocks' do browser = described_class.new(rom_data) reference = Rubyboy::EmulatorHeadless.new(rom_path) - reference_cycles = 0 - requested_cycles = 0 + reference_apu = reference.instance_variable_get(:@apu) + original_apu_step = reference_apu.method(:step) + samples = [] + reference_apu.define_singleton_method(:step) do |cycles| + complete = original_apu_step.call(cycles) + samples.concat(reference_apu.samples) if complete + complete + end + browser_ppu = browser.instance_variable_get(:@ppu) + original_ppu_step = browser_ppu.method(:step) + frame_complete = false + browser_ppu.define_singleton_method(:step) do |cycles| + frame_complete = original_ppu_step.call(cycles) + end produced_samples = 0 120.times do - frames = browser.run_cycles(70_224, 15, 15) - requested_cycles += 70_224 - expected_frames = 0 samples = [] - while reference_cycles < requested_cycles - cycles = reference.instance_variable_get(:@cpu).exec - reference_cycles += cycles - reference.instance_variable_get(:@timer).step(cycles) - apu = reference.instance_variable_get(:@apu) - samples.concat(apu.samples) if apu.step(cycles) - expected_frames += 1 if reference.instance_variable_get(:@ppu).step(cycles) + reference.step + browser_samples = [] + # A browser call can return early while Tobu disables the LCD. Compare + # completed PPU frames and retain audio from those bounded calls as well. + 120.times do + browser.step(15, 15) + browser_samples.concat(browser.audio_samples) + break if frame_complete end - expect(frames).to eq(expected_frames) - expect(browser.framebuffer).to eq(reference.framebuffer) - expect(browser.audio_samples).to eq(samples) + expect(frame_complete).to be(true) + expect(browser.framebuffer).to eq(reference.instance_variable_get(:@ppu).buffer) + expect(browser_samples).to eq(samples) expect(state(browser)).to eq(state(reference)) produced_samples += samples.length end expect(produced_samples).to be > 0 end - it 'carries instruction overshoot across small cycle budgets' do - split = described_class.new(rom_data) - whole = described_class.new(rom_data) - budgets = [1, 3, 7, 8192] * 100 - frames = 0 - samples = [] - budgets.each do |budget| - frames += split.run_cycles(budget, 15, 15) - samples.concat(split.audio_samples) - end - - expect(whole.run_cycles(budgets.sum, 15, 15)).to eq(frames) - expect(split.framebuffer).to eq(whole.framebuffer) - expect(samples).to eq(whole.audio_samples) - expect(state(split)).to eq(state(whole)) - end - it 'returns a framebuffer from step and keeps the partial APU block' do emulator = described_class.new(loop_rom) @@ -101,29 +94,26 @@ def loop_rom expect(emulator.instance_variable_get(:@ppu).hardware_state[:registers][:ly]).to eq(0) end - it 'uses active-low input masks and rejects invalid cycle budgets' do + it 'uses active-low input masks' do emulator = described_class.new(loop_rom) - expect(emulator.run_cycles(0, 14, 13)).to eq(0) + emulator.step(14, 13) + joypad = emulator.instance_variable_get(:@joypad).hardware_state expect(joypad[:direction_buttons]).to eq(0xfe) expect(joypad[:action_buttons]).to eq(0xfd) - expect(emulator.audio_samples).to be_empty - [-1, 1.5, '4'].each do |budget| - expect { emulator.run_cycles(budget, 15, 15) }.to raise_error(ArgumentError) - end end end RSpec.describe Executor do let(:rom_path) { File.expand_path('../../lib/roms/tobu.gb', __dir__) } - it 'writes little-endian pixels and stereo Float32 audio for the legacy frame call' do + it 'writes little-endian pixels and stereo Float32 audio for a frame call' do executor = described_class.new(rom_path) writes = {} allow(File).to receive(:binwrite) { |path, bytes| writes[path] = bytes } - expect(executor.exec).to eq(1) + executor.exec emulator = executor.instance_variable_get(:@emulator) expect(writes.fetch('/video.data').unpack('V*')).to eq(emulator.framebuffer) @@ -134,16 +124,16 @@ def loop_rom end end - it 'writes a frame only after the PPU completes it and skips empty audio' do + it 'writes the framebuffer and skips empty audio' do executor = described_class.new(rom_path) + emulator = executor.instance_variable_get(:@emulator) + allow(emulator).to receive(:audio_samples).and_return([]) allow(File).to receive(:binwrite) - expect(executor.exec_cycles(1)).to eq(0) + executor.exec - expect(File).not_to have_received(:binwrite) - expect(executor.exec_cycles(70_224)).to eq(1) expect(File).to have_received(:binwrite).with('/video.data', a_string_matching(/./m)) - expect(File).to have_received(:binwrite).with('/audio.data', a_string_matching(/./m)) + expect(File).not_to have_received(:binwrite).with('/audio.data', anything) end it 'uses the existing packed ROM directory' do diff --git a/spec/rubyboy/hardware_state_spec.rb b/spec/rubyboy/hardware_state_spec.rb index 08c1587..0521a51 100644 --- a/spec/rubyboy/hardware_state_spec.rb +++ b/spec/rubyboy/hardware_state_spec.rb @@ -57,6 +57,8 @@ def rom_with_ram_size(byte) shared_examples 'a compatible channel snapshot' do |channel_class, fields| it 'preserves the saved field set and restores non-default values' do channel = channel_class.new + expect(channel.hardware_state.keys).to match_array(channel.instance_variables.map { _1.to_s.delete_prefix('@').to_sym }) + expected = fields.each_with_index.to_h do |field, index| initial = channel.instance_variable_get("@#{field}") value = case initial diff --git a/spec/rubyboy/lcd_spec.rb b/spec/rubyboy/lcd_spec.rb index fe1858c..3d89e0d 100644 --- a/spec/rubyboy/lcd_spec.rb +++ b/spec/rubyboy/lcd_spec.rb @@ -10,7 +10,6 @@ allow(Rubyboy::SDL).to receive(:CreateTexture).and_return(pointer) allow(Rubyboy::SDL).to receive(:SetHint) allow(Rubyboy::SDL).to receive(:RenderSetLogicalSize) - allow(Rubyboy::SDL).to receive(:RenderSetVSync).and_return(0) allow(Rubyboy::SDL).to receive(:DestroyWindow) allow(Rubyboy::SDL).to receive(:Quit) end @@ -27,47 +26,4 @@ expect(lcd.window_should_close?).to be(true) end - - describe '#initialize' do - it 'keeps the existing renderer configuration by default' do - expect(Rubyboy::SDL).not_to receive(:RenderSetVSync) - - described_class.new - end - - it 'disables VSync on the created renderer before preparing its texture' do - expect(Rubyboy::SDL).to receive(:CreateRenderer).with(pointer, -1, 0).ordered.and_return(pointer) - expect(Rubyboy::SDL).to receive(:RenderSetVSync).with(pointer, 0).ordered.and_return(0) - expect(Rubyboy::SDL).to receive(:CreateTexture).ordered.and_return(pointer) - - described_class.new(vsync: false) - end - - it 'can explicitly enable VSync' do - expect(Rubyboy::SDL).to receive(:RenderSetVSync).with(pointer, 1).and_return(0) - - described_class.new(vsync: true) - end - - it 'fails if the renderer cannot apply the requested VSync setting' do - allow(Rubyboy::SDL).to receive(:RenderSetVSync).and_return(-1) - allow(Rubyboy::SDL).to receive(:GetError).and_return('VSync is unsupported') - expect(Rubyboy::SDL).not_to receive(:CreateTexture) - expect(Rubyboy::SDL).to receive(:DestroyWindow).with(pointer) - expect(Rubyboy::SDL).to receive(:Quit) - - expect { described_class.new(vsync: false) }.to raise_error(RuntimeError, 'VSync is unsupported') - end - - it 'reports renderer creation failures before trying to configure VSync' do - allow(Rubyboy::SDL).to receive(:CreateRenderer).and_return(FFI::Pointer::NULL) - allow(Rubyboy::SDL).to receive(:GetError).and_return('Renderer creation failed') - expect(Rubyboy::SDL).not_to receive(:RenderSetVSync) - expect(Rubyboy::SDL).not_to receive(:CreateTexture) - expect(Rubyboy::SDL).to receive(:DestroyWindow).with(pointer) - expect(Rubyboy::SDL).to receive(:Quit) - - expect { described_class.new(vsync: false) }.to raise_error(RuntimeError, 'Renderer creation failed') - end - end end diff --git a/spinel/README.md b/spinel/README.md new file mode 100644 index 0000000..4980869 --- /dev/null +++ b/spinel/README.md @@ -0,0 +1,44 @@ +# Spinel + +[Spinel](https://github.com/matz/spinel) compiles Ruby to C. The existing +`exe/rubyboy` and `exe/rubyboy-bench` compile without changes into native +executables, so Rubyboy can be played and benchmarked without CRuby. +For the browser version built with Spinel, see [wasm/README.md](../wasm/README.md#spinel-build). + +## Build Spinel + +Spinel needs a C compiler, `make` and Ruby to build. These commands put it in +`../spinel`, next to this repository: + +```sh +git clone https://github.com/matz/spinel.git ../spinel +make -C ../spinel deps +make -C ../spinel +``` + +## Play + +From the repository root: + +```sh +mkdir -p build/spinel +../spinel/bin/spinel -I lib exe/rubyboy -o build/spinel/rubyboy +build/spinel/rubyboy lib/roms/tobu.gb +``` + +The executable uses SDL2 like the gem, and accepts the same keys and options +(see [README.md](../README.md#usage)). + +## Benchmark + +```sh +mkdir -p build/spinel +../spinel/bin/spinel -I lib exe/rubyboy-bench -o build/spinel/rubyboy-bench +build/spinel/rubyboy-bench --rom-path lib/roms/tobu.gb --frames 1500 --warmup-frames 1500 --count 5 +``` + +See [BENCHMARK.md](../BENCHMARK.md) for the options and how to compare runtimes. + +## Verified with + +Spinel `2810a23` (matz/spinel, 2026-10-08) on macOS (Apple Silicon). diff --git a/wasm/README.md b/wasm/README.md index 9ddf710..8441dff 100644 --- a/wasm/README.md +++ b/wasm/README.md @@ -1,93 +1,135 @@ -# Spinel WebAssembly build +# WebAssembly builds + +Rubyboy runs in the browser in two builds that share the same web page and +JavaScript. Both run the same Ruby code: `lib/executor.rb` and +`Rubyboy::EmulatorWasm`. + +| Build | URL | How the Ruby code runs | Wasm file | +| --- | --- | --- | --- | +| ruby.wasm | https://sacckey.github.io/rubyboy/ | CRuby compiled to WebAssembly, with the Rubyboy sources packed in | `docs/rubyboy.wasm` | +| Spinel | https://sacckey.github.io/rubyboy/spinel/ | Ruby compiled to C by Spinel, then to WebAssembly | `docs/spinel/rubyboy-spinel.wasm` | + +## How it works + +| File | Role | +| --- | --- | +| `docs/index.html`, `docs/index.js` | Page, keyboard and touch input, Canvas drawing, audio playback | +| `docs/worker.js`, `docs/spinel/worker.js` | Download the build's Wasm and create the adapter | +| `docs/emulation-worker.js` | Worker messages: start, input, ROM loading, playback options | +| `docs/emulation-loop.js` | Frame pacing | +| `docs/rubyboy-vm.js` | Calls the Ruby `Executor` and reads `/video.data` and `/audio.data` from the WASI file system | +| `docs/audio-worklet.js` | Audio buffering and resampling | +| `docs/spinel/rubyboy-spinel.mjs` | Puts the bundled files in the WASI file system and calls the Spinel build's exported functions | +| `wasm/spinel_main.rb` | Entry points that Spinel compiles from the existing `Executor` | +| `wasm/glue.c` | Exports those entry points to JavaScript and reports Ruby exceptions | + +The worker runs one frame per task. With the 60 FPS limit, frames are paced at +70,224 / 4,194,304 seconds (about 59.73 FPS); a late or slow frame does not cause +catch-up frames. A game with the LCD turned off still returns after one frame of +CPU time. + +The APU always runs. Its 48 kHz stereo samples are sent to an AudioWorklet that +buffers up to 2,048 frames, drops the oldest samples when full, and resamples if +the device rate differs. When muted, samples are generated but not sent. + +## Run locally -This version compiles the existing `lib/executor.rb` and `Rubyboy::EmulatorWasm` -through **Ruby → Spinel-generated C → WebAssembly**. CPU, PPU, APU, cartridge -logic, framebuffer `pack('V*')`, audio `pack('e*')` and `File.binwrite` are the -same Ruby code used by the normal packed CRuby version. There is no handwritten -Rubyboy C implementation or Spinel-specific emulator class. +```sh +python3 -m http.server 8765 --bind 127.0.0.1 --directory docs +``` + +Open http://127.0.0.1:8765/ (ruby.wasm) or http://127.0.0.1:8765/spinel/ (Spinel). +Each page loads its local Wasm file first. If that file is missing, it loads the +asset of the latest GitHub Release through `https://proxy.sacckey.dev/`, so the +pages work without building anything. + +## Playback options -`wasm/spinel_main.rb` only declares callable entry points over the existing -Executor. Spinel generates their browser ABI with `--ext wasm`; its generic -JavaScript host supplies WASI and virtual files without emulator-specific -knowledge. Its host source is maintained in Spinel and copied during the build. +The **Limit to 60 FPS** and **Mute** checkboxes change playback while running. +Both are checked by default; URL parameters set their initial state. + +| URL | Speed | Sound | +| --- | --- | --- | +| `/` | 60 FPS | Muted | +| `/?throttle=0` | Unlimited | Muted | +| `/?mute=0` | 60 FPS | Starts at the first key press or click | + +## ruby.wasm build + +The build needs Ruby, a native C/C++ toolchain, `make`, `curl` and `tar` +(on macOS, the Xcode Command Line Tools). From the repository root: + +```sh +BUNDLE_IGNORE_CONFIG=1 BUNDLE_ONLY=wasm bundle install +ruby wasm/rubyboy-wasm build +ruby wasm/rubyboy-wasm pack +``` -Both versions use the same `docs/rubyboy-vm.js`, emulation worker and scheduler, -UI, Canvas rendering and AudioWorklet. The shared adapter accepts a callable -Executor and a value converter: CRuby calls its Ruby object, while Spinel calls -compiled entry points. Both read the same `/video.data` and `/audio.data` files, -copy transferable buffers, and retain APU generation and Float32 packing when -muted. The normal version keeps its local-first download and proxy fallback. +- `build` compiles CRuby for WebAssembly into `docs/ruby-js.wasm`. The first run + downloads the WASI SDK and Ruby sources and takes several minutes; caches stay + in `build/` and `rubies/`. The Ruby version is ruby_wasm's `4.0` (Ruby 4.0.0). +- `pack` writes `docs/rubyboy.wasm` with the Git-tracked files under `lib/` + mounted at `/lib`, and removes C debug information. Use `pack --keep-debug` + to keep it. Run `pack` again after changing anything under `lib/`. +- The JavaScript SDK version in `docs/worker.js` (`@ruby/wasm-wasi@2.10.1`) must + match the `js` gem in the Gemfile. Update both together. +- YJIT is not available inside the Wasm. -## Build and run +## Spinel build -Use a Spinel checkout containing the `--ext wasm` browser support added with -this change, Python 3, Make, a native C compiler, and WASI SDK 34 or later. -`bundle install`, SDL and Emscripten are not required for the Spinel build. -From Rubyboy, with Spinel in `../spinel` and the SDK already installed: +The browser build uses upstream Spinel, built in `../spinel` as described in +[spinel/README.md](../spinel/README.md#build-spinel). It also needs Python 3, +[WASI SDK 34](https://github.com/WebAssembly/wasi-sdk/releases/tag/wasi-sdk-34) +or later, and a browser that supports WebAssembly exception handling. ```sh -make -C ../spinel deps python3 wasm/build.py --spinel-dir ../spinel --wasi-sdk /path/to/wasi-sdk-34 -python3 -m http.server 8765 --bind 127.0.0.1 --directory docs ``` -Open **http://127.0.0.1:8765/spinel/**. Under GitHub Pages the path is -`/rubyboy/spinel/`. The root page continues to use packed `rubyboy.wasm`. -The Spinel worker first tries local `./rubyboy-spinel.wasm`, then falls back to -`https://proxy.sacckey.dev/rubyboy-spinel.wasm` when the local response fails. -To use the proxy, upload the packed `rubyboy-spinel.wasm` as a GitHub Release -asset alongside the normal `rubyboy.wasm`. -`?throttle=0&mute=1` starts either version in unlimited, muted mode. - -WASI SDK archives are available from the official -[wasi-sdk 34 release](https://github.com/WebAssembly/wasi-sdk/releases/tag/wasi-sdk-34). -Pass the extracted host-specific directory to `--wasi-sdk`; `WASI_SDK` and -`SPINEL_DIR` can supply defaults. The build prints its commands and rebuilds -the target runtime to avoid reusing objects from another SDK or optimization -level. A browser supporting standardized Wasm exception handling is required. - -## Generated assets - -- `build/spinel-wasm/rubyboy-spinel.wasm`: compiled Executor and generic host ABI. -- `docs/spinel/rubyboy-spinel.wasm`: that module with the original `lib/roms` files - embedded by Spinel's generic `scripts/wasm-pack.py`. -- `docs/spinel/spinel-vm.mjs`: an unmodified copy of Spinel's generic host. -- `docs/spinel/index.html`: generated from the existing root HTML with relative - asset paths adjusted; the visible UI is shared. -- `docs/spinel/build-info.json`: compiler identities and source/artifact hashes. - -The generated HTML and generic JavaScript host are committed so GitHub Pages -can serve `/rubyboy/spinel/` directly from `main` and `docs/`. Commit their -updated copies when rebuilding after root HTML or Spinel changes. Wasm binaries -and `build-info.json` remain ignored by Git; publish the packed Wasm as a GitHub -Release asset for the proxy fallback. ROMs are mounted read-only from the Wasm's -file section: no deployed ROM copies or separate ROM downloads are needed. The host uses the same pinned `browser_wasi_shim` 0.4.2 CDN -module as the normal version. Retain the full source changes when reproducing -an uncommitted build; a base Git revision alone does not identify those changes. - -## Verification - -For the normal runtime test, first build and pack `docs/rubyboy.wasm` from the -current sources using the [normal build instructions](../docs/wasm-playback.md). -For local tests, install the same browser dependencies into an ignored directory: +`build.py` builds Spinel's WebAssembly runtime, compiles `wasm/spinel_main.rb` to C +with callable entry points (`--ext-init` and `--ext-entry`), links it with +`wasm/glue.c` using the flags from `spinel --print-build`, and appends the +Git-tracked files in `lib/roms` as a `rubyboy-files` custom section. +`SPINEL_DIR` and `WASI_SDK` can be used instead of the options. + +| Generated file | In Git | Contents | +| --- | --- | --- | +| `build/spinel-wasm/rubyboy.c`, `rubyboy.h` | No | Spinel's C output | +| `build/spinel-wasm/rubyboy-spinel.wasm` | No | Linked module without files | +| `docs/spinel/rubyboy-spinel.wasm` | No | The above with the Git-tracked files in `lib/roms` appended | +| `docs/spinel/index.html` | Yes | `docs/index.html` with asset paths and page description adjusted | + +## Tests + +The two Wasm tests load these packages from the Git-ignored `build/` directory: ```sh -npm install --prefix build/browser-runtime --no-save \ +npm install --prefix build/browser-runtime --no-save --no-audit --no-fund \ @ruby/wasm-wasi@2.10.1 @bjorn3/browser_wasi_shim@0.4.2 -node scripts/test_wasm_frontend.cjs -node --experimental-wasm-exnref wasm/test.mjs -node scripts/test_wasm_runtime.mjs ``` -The tests use Ruby from `PATH`; validation used Ruby 4.0.7 and Node 24.4.0. -`wasm/test.mjs` executes the actual Spinel module through the same RubyboyVM -adapter. Frame and cycle-budget scenarios use the existing CRuby reference; -framebuffer and Float32 audio bytes must match at six checkpoints in each -scenario. It also checks embedded files, uploads, ROM switching, exception -recovery, transfer copies, LCD-off return and 1500 continuous frames. - -Spinel's separate generic host tests use a small Ruby program with no emulator -code, covering ordinary values, binary and text Strings, GC, exceptions and -virtual file operations. These demonstrate that the browser connection is -reusable independently of Rubyboy. This change does not include benchmark -results or claim an end-to-end speedup from playback FPS alone. +| Command | Tests | Needs | +| --- | --- | --- | +| `node wasm/test/frontend.cjs` | Frame pacing, input latching, audio buffering | Nothing | +| `bundle exec rspec spec/rubyboy/emulator_wasm_spec.rb` | `EmulatorWasm` and `Executor` in CRuby | Nothing | +| `node wasm/test/ruby_wasm.mjs` | Packed `/lib` files match Git, ROM loading and frames in ruby.wasm | `pack`, the packages above | +| `node --experimental-wasm-exnref wasm/test/spinel.mjs` | Spinel output matches CRuby, embedded files match Git, ROM loading | Spinel build, the packages above, `ruby` on `PATH` | + +## Deploy + +GitHub Pages serves `docs/` from `main`. The Wasm files are not in Git, so the +published pages load them from the proxy, which serves the latest GitHub +Release's assets. + +1. Commit changes to `docs/`, including a regenerated `docs/spinel/index.html`. + If `wasm/glue.c` or `docs/spinel/rubyboy-spinel.mjs` changed, do steps 2 and 3 + right after the change reaches `main`; the page and the Wasm must match. +2. Upload the Wasm files to the latest release: + + ```sh + gh release upload "$(gh release view --json tagName -q .tagName)" \ + docs/rubyboy.wasm docs/spinel/rubyboy-spinel.wasm --clobber + ``` + +3. Purge the proxy cache for `/rubyboy.wasm` and `/rubyboy-spinel.wasm`. + Otherwise it keeps serving the old files for up to four hours. diff --git a/wasm/build.py b/wasm/build.py index e0b4355..49c3267 100644 --- a/wasm/build.py +++ b/wasm/build.py @@ -1,16 +1,14 @@ #!/usr/bin/env python3 -"""Compile the existing Ruby browser executor using Spinel's generic Wasm host.""" +"""Compile the browser Executor with upstream Spinel and the WASI SDK.""" import argparse -import hashlib -import json import os from pathlib import Path import shlex -import shutil +import struct import subprocess ROOT = Path(__file__).resolve().parent.parent -ENTRIES = ('init', 'exec', 'exec_cycles', 'read_rom_from_virtual_fs', 'read_pre_installed_rom') +ENTRIES = ('init', 'exec', 'read_rom_from_virtual_fs', 'read_pre_installed_rom') def run(argv, **kwargs): @@ -19,8 +17,23 @@ def run(argv, **kwargs): subprocess.run(argv, check=True, **kwargs) -def digest(path): - return hashlib.sha256(path.read_bytes()).hexdigest() +def leb128(value): + out = bytearray() + while True: + byte, value = value & 0x7f, value >> 7 + out.append(byte | (0x80 if value else 0)) + if not value: + return bytes(out) + + +def files_section(paths): + """A 'rubyboy-files' custom section: (path length, path, size, bytes) per file.""" + payload = bytearray() + for path in paths: + name, data = ('/' + path).encode(), (ROOT / path).read_bytes() + payload += struct.pack(' Spinel generated C and Wasm host -> WebAssembly', - 'spinel': subprocess.check_output([compiler, '--version'], text=True).strip(), - 'clang': subprocess.check_output([clang, '--version'], text=True).splitlines()[0], - 'rubyboy_revision': subprocess.check_output(['git', '-C', ROOT, 'rev-parse', 'HEAD'], text=True).strip(), - 'spinel_revision': subprocess.check_output(['git', '-C', spinel, 'rev-parse', 'HEAD'], text=True).strip(), - 'rubyboy_diff_sha256': hashlib.sha256(subprocess.check_output(['git', '-C', ROOT, 'diff', 'HEAD'])).hexdigest(), - 'spinel_diff_sha256': hashlib.sha256(subprocess.check_output(['git', '-C', spinel, 'diff', 'HEAD'])).hexdigest(), - 'executor_sha256': digest(ROOT / 'lib/executor.rb'), - 'emulator_sha256': digest(ROOT / 'lib/rubyboy/emulator_wasm.rb'), - 'host_sha256': digest(host), - 'packer_sha256': digest(spinel / 'scripts/wasm-pack.py'), - 'entry_sha256': digest(ROOT / 'wasm/spinel_main.rb'), - 'roms': [{'path': '/lib/roms/' + path.relative_to(ROOT / 'lib/roms').as_posix(), - 'bytes': path.stat().st_size, 'sha256': digest(path)} - for path in sorted((ROOT / 'lib/roms').rglob('*')) if path.is_file()], - 'wasm_bytes': (output / 'rubyboy-spinel.wasm').stat().st_size, - 'wasm_sha256': digest(output / 'rubyboy-spinel.wasm'), - } - (output / 'build-info.json').write_text(json.dumps(metadata, indent=2) + '\n') - print(f"Built {output / 'rubyboy-spinel.wasm'}: {metadata['wasm_bytes']:,} bytes") + size = (output / 'rubyboy-spinel.wasm').stat().st_size + print(f"Built {output / 'rubyboy-spinel.wasm'}: {size:,} bytes") if __name__ == '__main__': diff --git a/wasm/glue.c b/wasm/glue.c new file mode 100644 index 0000000..f1d950b --- /dev/null +++ b/wasm/glue.c @@ -0,0 +1,43 @@ +/* Export RubyboyBrowser's Spinel entry points to JavaScript. */ +#include +#include "rubyboy.h" + +#define EXPORT(name) __attribute__((export_name(name))) + +enum entry { INIT, EXEC, READ_UPLOADED_ROM, READ_BUNDLED_ROM }; +typedef struct { enum entry entry; const char *name; int direction, action; } call; +static char error[1024]; + +static void invoke(void *opaque) { + call *c = opaque; + switch (c->entry) { + case INIT: sp_RubyboyBrowser_s_init(c->name); break; + case EXEC: sp_RubyboyBrowser_s_exec(c->direction, c->action); break; + case READ_UPLOADED_ROM: sp_RubyboyBrowser_s_read_rom_from_virtual_fs(); break; + case READ_BUNDLED_ROM: sp_RubyboyBrowser_s_read_pre_installed_rom(c->name); break; + } +} + +/* Returns 0, or 1 with the Ruby exception in rubyboy_error(). */ +static int run(call c) { + const char *cls = NULL, *msg = NULL; + if (!Init_rubyboy_browser_try(invoke, &c, &cls, &msg)) return 0; + snprintf(error, sizeof error, "%s: %s", cls ? cls : "Error", msg ? msg : ""); + return 1; +} + +EXPORT("rubyboy_init") int rubyboy_init(const char *rom_path) { + static int initialized; + if (!initialized) { Init_rubyboy_browser(); initialized = 1; } + return run((call){INIT, rom_path}); +} +EXPORT("rubyboy_exec") int rubyboy_exec(int direction, int action) { + return run((call){EXEC, NULL, direction, action}); +} +EXPORT("rubyboy_read_rom_from_virtual_fs") int rubyboy_read_rom_from_virtual_fs(void) { + return run((call){READ_UPLOADED_ROM}); +} +EXPORT("rubyboy_read_pre_installed_rom") int rubyboy_read_pre_installed_rom(const char *name) { + return run((call){READ_BUNDLED_ROM, name}); +} +EXPORT("rubyboy_error") const char *rubyboy_error(void) { return error; } diff --git a/wasm/rubyboy-wasm b/wasm/rubyboy-wasm new file mode 100755 index 0000000..fcf7fed --- /dev/null +++ b/wasm/rubyboy-wasm @@ -0,0 +1,62 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +case ARGV +when ['build'] + command = %w[build --ruby-version 4.0 -o ./docs/ruby-js.wasm] +when ['pack'], ['pack', '--keep-debug'] + command = %w[pack ./docs/ruby-js.wasm --dir ./lib::/lib -o ./docs/rubyboy.wasm] +else + puts "Invalid argument. Use 'build' or 'pack [--keep-debug]'." + exit 1 +end + +ENV['BUNDLE_ONLY'] = 'wasm' +ENV['BUNDLE_IGNORE_CONFIG'] = '1' + +require 'bundler/setup' +require 'fileutils' +require 'tempfile' +require 'tmpdir' +require 'ruby_wasm' +require 'ruby_wasm/cli' + +# Exclude all gems except the 'js' gem for packaging +definition = Bundler.definition +excluded_gems = definition.resolve.materialize(definition.requested_dependencies).map(&:name) +excluded_gems -= %w[js] +RubyWasm::Packager::EXCLUDED_GEMS.concat(excluded_gems) + +lib_copy = nil +if command.first == 'pack' + # Pack only Git-tracked files so local saves and states stay out of the Wasm. + tracked = IO.popen(%w[git ls-files -z lib], &:read).split("\0") + raise 'git ls-files failed' unless Process.last_status.success? && !tracked.empty? + + lib_copy = Dir.mktmpdir('rubyboy-lib-') + tracked.each do |path| + FileUtils.mkdir_p(File.join(lib_copy, File.dirname(path))) + FileUtils.cp(path, File.join(lib_copy, path)) + end + command[command.index('./lib::/lib')] = "#{lib_copy}/lib::/lib" +end + +begin + RubyWasm::CLI.new(stdout: $stdout, stderr: $stderr).run(command) +ensure + FileUtils.rm_rf(lib_copy) if lib_copy +end + +# Keep executable sections unchanged while removing unused C debug information. +# The SDK is installed by the build command; --keep-debug retains the full module. +if ARGV == ['pack'] + build_root = File.expand_path(ENV['RUBY_WASM_ROOT'] || Bundler.root.to_s) + objcopy = File.join(build_root, 'build', 'toolchain', 'wasi-sdk-24.0', 'bin', 'llvm-objcopy') + raise "WASI SDK 24.0 is missing; run 'ruby wasm/rubyboy-wasm build' first or use 'pack --keep-debug'" unless File.executable?(objcopy) + + wasm_path = './docs/rubyboy.wasm' + Tempfile.create(['rubyboy-', '.wasm'], File.dirname(wasm_path)) do |output| + system(objcopy, '--strip-debug', wasm_path, output.path, exception: true) + FileUtils.mv(output.path, wasm_path) + end +end diff --git a/wasm/spinel_main.rb b/wasm/spinel_main.rb index 948f374..ab839d9 100644 --- a/wasm/spinel_main.rb +++ b/wasm/spinel_main.rb @@ -11,10 +11,7 @@ def self.init(rom_path) def self.exec(direction, action) @executor.exec(direction, action) - end - - def self.exec_cycles(cycles, direction, action) - @executor.exec_cycles(cycles, direction, action) + true end def self.read_rom_from_virtual_fs @@ -32,7 +29,6 @@ def self.read_pre_installed_rom(name) if __FILE__ == $PROGRAM_NAME RubyboyBrowser.init(File.expand_path('../lib/roms/tobu.gb', __dir__)) p RubyboyBrowser.exec(15, 15) - p RubyboyBrowser.exec_cycles(32768, 15, 15) p RubyboyBrowser.read_pre_installed_rom('tobu.gb') p RubyboyBrowser.read_rom_from_virtual_fs end diff --git a/wasm/test.mjs b/wasm/test.mjs deleted file mode 100644 index d4c32df..0000000 --- a/wasm/test.mjs +++ /dev/null @@ -1,105 +0,0 @@ -// Execute both backends through the shared RubyboyVM and compare Ruby outputs. -// node --experimental-wasm-exnref wasm/test.mjs -import assert from 'node:assert/strict'; -import { readFileSync } from 'node:fs'; -import { createHash } from 'node:crypto'; -import { spawnSync } from 'node:child_process'; -import * as shim from '../build/browser-runtime/node_modules/@bjorn3/browser_wasi_shim/dist/index.js'; -import { createSpinelVM } from '../docs/spinel/spinel-vm.mjs'; -import { RubyboyVM } from '../docs/rubyboy-vm.js'; - -const hash = bytes => createHash('sha256').update(new Uint8Array(bytes)).digest('hex'); -const native = spawnSync(process.env.RUBY || 'ruby', ['scripts/wasm_reference.rb'], { - encoding: 'utf8', maxBuffer: 1024 * 1024, -}); -assert.equal(native.status, 0, native.stderr); -const reference = JSON.parse(native.stdout); -const bytes = readFileSync('docs/spinel/rubyboy-spinel.wasm'); -const module = await WebAssembly.compile(bytes); -assert(WebAssembly.Module.imports(module).every(entry => entry.module === 'wasi_snapshot_preview1')); -const { vm, root } = await createSpinelVM(module, { shim }); -assert.equal(vm.call('RubyboyBrowser.init', '/lib/roms/tobu.gb'), true); -const executor = { call: (method, ...args) => vm.call(`RubyboyBrowser.${method}`, ...args) }; -const core = new RubyboyVM(vm, root, shim.File, { executor, toValue: value => value }); -const roms = root.contents.get('lib').contents.get('roms'); -const info = JSON.parse(readFileSync('docs/spinel/build-info.json')); -for (const entry of info.roms) { - const relative = entry.path.slice('/lib/roms/'.length); - let file = roms; - for (const part of relative.split('/')) file = file.contents.get(part); - assert(file instanceof shim.File && file.readonly); - assert.equal(file.data.length, entry.bytes); - assert.equal(hash(file.data), entry.sha256); - assert.equal(hash(file.data), hash(readFileSync('lib/roms/' + relative))); -} -assert.equal(info.executor_sha256, hash(readFileSync('lib/executor.rb'))); -assert.equal(info.emulator_sha256, hash(readFileSync('lib/rubyboy/emulator_wasm.rb'))); -assert.equal(info.wasm_sha256, hash(bytes)); -const rom = readFileSync('lib/roms/tobu.gb'); -for (const scenario of reference.scenarios) { - core.loadUploadedRom(Uint8Array.from(rom).buffer); - assert.equal(root.contents.has('rom.data'), false); - assert.equal(core.popAudio(), null); - const accumulated = []; - let frames = 0; - let audioBytes = 0; - for (let tick = 1; tick <= reference.ticks; tick++) { - const [direction, action] = reference.inputs[tick - 1]; - const blocks = []; - if (scenario.mode === 'frame') { - frames += core.runFrame(direction, action); - const block = core.popAudio(); - if (block) blocks.push(Buffer.from(block)); - } else { - for (const budget of reference.cycle_budgets) { - frames += core.runCycles(budget, direction, action); - const block = core.popAudio(); - if (block) blocks.push(Buffer.from(block)); - } - } - const audio = Buffer.concat(blocks); - accumulated.push(audio); - audioBytes += audio.length; - assert.equal(core.popAudio(), null); - const expected = scenario.checkpoints.find(entry => entry.tick === tick); - if (!expected) continue; - const file = root.contents.get('video.data'); - const video = core.framebuffer(); - assert.deepEqual({ tick, frames, video_sha256: hash(video), - audio_sha256: hash(audio), audio_bytes: audio.length, - accumulated_audio_sha256: hash(Buffer.concat(accumulated)), - accumulated_audio_bytes: audioBytes }, expected, `${scenario.mode} tick ${tick}`); - const transferred = structuredClone(video, { transfer: [video] }); - assert.equal(video.byteLength, 0); - assert.equal(hash(transferred), hash(file.data)); - } - assert(audioBytes > 0); - console.log(`PASS ${scenario.mode}: identical Ruby framebuffer/audio bytes at six checkpoints`); -} -for (const name of ['tobu.gb', 'bgbtest.gb']) { - core.loadPreInstalledRom(name); - assert.equal(core.runFrame(15, 15), 1); - assert.equal(core.framebuffer().byteLength, 160 * 144 * 4); -} -assert.throws(() => core.loadPreInstalledRom('../other.gb'), /Unknown bundled ROM/); -assert.throws(() => core.loadUploadedRom(new ArrayBuffer(5)), /ROM size/); -assert.throws(() => core.runCycles(32769, 15, 15), /Invalid cycle budget/); -const unsupported = Uint8Array.from(rom); -unsupported[0x147] = 0xff; -assert.throws(() => core.loadUploadedRom(unsupported.buffer)); -assert.equal(root.contents.has('rom.data'), false); -core.loadUploadedRom(Uint8Array.from(rom).buffer); -for (let frame = 0; frame < 1500; frame++) { - assert.equal(core.runFrame(15, 15), 1); - assert.equal(core.framebuffer().byteLength, 160 * 144 * 4); - core.popAudio(); -} -const lcdOff = Uint8Array.from(rom.subarray(0, 32768)); -lcdOff[0x147] = lcdOff[0x148] = lcdOff[0x149] = 0; -lcdOff.set([0xc3, 0x50, 0x01], 0x100); -lcdOff.set([0x3e, 0, 0xe0, 0x40, 0x18, 0xfe], 0x150); -core.loadUploadedRom(lcdOff.buffer); -assert.equal(core.runFrame(15, 15), 1); -assert.equal(core.framebuffer().byteLength, 160 * 144 * 4); -assert(core.popAudio().byteLength > 0); -console.log(`PASS embedded files, unchanged Executor, shared adapter, ROM/error recovery, LCD-off and 1500 frames (${bytes.length.toLocaleString()} bytes)`); diff --git a/wasm/test/frontend.cjs b/wasm/test/frontend.cjs new file mode 100644 index 0000000..6a79720 --- /dev/null +++ b/wasm/test/frontend.cjs @@ -0,0 +1,179 @@ +// Frame pacing, input latching and audio buffering in the shared browser code. +// Run: node wasm/test/frontend.cjs +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); +const docs = path.resolve(__dirname, '../../docs'); + +const FRAME_MS = 70224 / 4194304 * 1000; + +function loopHarness(EmulationLoop, {frameMs = 0, timerTurnaround = 0} = {}) { + let time = 0; + let nextTimer = 0; + let audioDrains = 0; + const timers = new Map(); + const calls = []; + const messages = []; + const frames = []; + const adapter = { + runFrame(direction, action) { + calls.push({direction, action, startedAt: time}); + time += frameMs; + }, + framebuffer() { const buffer = new ArrayBuffer(160 * 144 * 4); frames.push(buffer); return buffer; }, + popAudio() { audioDrains++; return new Float32Array([.25, -.25]).buffer; }, + }; + const loop = new EmulationLoop(adapter, (message, transfers) => messages.push({message, transfers}), { + now: () => time, + setTimeout: (callback, delay) => { timers.set(++nextTimer, {callback, delay}); return nextTimer; }, + clearTimeout: handle => timers.delete(handle), + }); + function advance(now) { + time = now; + const [handle, task] = timers.entries().next().value; + timers.delete(handle); + task.callback(); + } + return { + loop, adapter, calls, messages, frames, timers, + drains: () => audioDrains, + now: () => time, + nextDelay: () => timers.values().next().value.delay, + advance, + runNext() { advance(time + Math.max(timerTurnaround, timers.values().next().value.delay)); }, + }; +} + +function testClock(EmulationLoop) { + const fast = loopHarness(EmulationLoop, {frameMs: 2}); + fast.loop.start(); + fast.loop.start(); + assert.equal(fast.timers.size, 1, 'start is idempotent'); + assert.equal(fast.calls.length, 1, 'the first frame is presented immediately'); + assert.equal(fast.nextDelay(), Math.ceil(FRAME_MS - 2), 'execution time is included in the frame interval'); + fast.advance(10); + assert.equal(fast.calls.length, 1, 'an early timer must not run Ruby'); + assert.equal(fast.drains(), 1); + fast.advance(FRAME_MS); + assert.equal(fast.calls.length, 2); + for (let i = 0; i < 240; i++) fast.runNext(); + const rate = (fast.calls.length - 1) * 1000 / (fast.calls.at(-1).startedAt - fast.calls[0].startedAt); + assert.ok(rate > 59 && rate <= 60, `fast adapters are capped near the Game Boy frame rate: ${rate}`); + const fastPixels = fast.messages.filter(item => item.message.type === 'pixelData'); + assert.equal(fastPixels.length, fast.calls.length); + assert.equal(fast.messages.filter(item => item.message.type === 'audioData').length, 0, 'mute is the default'); + assert.equal(fast.drains(), fast.calls.length, 'audio is drained once per frame'); + fast.loop.stop(); + assert.equal(fast.timers.size, 0, 'stop cancels the loop'); + + const paused = loopHarness(EmulationLoop, {frameMs: 2}); + paused.loop.start(); + paused.advance(1000); + assert.equal(paused.calls.length, 2, 'a one-second timer pause runs one frame, without catching up'); + assert.equal(paused.nextDelay(), Math.ceil(FRAME_MS - 2), 'pause recovery resumes ordinary pacing'); + paused.loop.stop(); + paused.loop.start(); + assert.equal(paused.calls.length, 3, 'restart resets the deadline instead of replaying elapsed time'); + paused.loop.stop(); + + const slow = loopHarness(EmulationLoop, {frameMs: 28, timerTurnaround: 4}); + const unlimited = loopHarness(EmulationLoop, {frameMs: 28, timerTurnaround: 4}); + unlimited.loop.setThrottle(false); + slow.loop.start(); + unlimited.loop.start(); + for (let i = 0; i < 100; i++) { + assert.equal(slow.nextDelay(), 0, 'a frame slower than the limit incurs no extra wait'); + const before = slow.calls.length; + slow.runNext(); + unlimited.runNext(); + assert.equal(slow.calls.length, before + 1, 'slow hosts yield after every frame'); + } + assert.equal(slow.calls.length, unlimited.calls.length); + assert.equal(slow.now(), unlimited.now(), 'limited slow hosts have the same throughput as unlimited hosts'); + assert.equal(slow.messages.filter(item => item.message.type === 'pixelData').length, slow.calls.length); + const packet = unlimited.messages.find(item => item.message.type === 'pixelData'); + assert.equal(packet.message.data, unlimited.frames[0]); + assert.equal(packet.transfers[0], packet.message.data); + slow.loop.stop(); + unlimited.loop.stop(); + + const toggled = loopHarness(EmulationLoop); + toggled.loop.setMuted(false); + toggled.loop.start(); + toggled.loop.setThrottle(false); + toggled.advance(1); + assert.equal(toggled.calls.length, 2, 'switching off the limit permits the next task'); + assert.equal(toggled.nextDelay(), 0); + toggled.loop.setThrottle(true); + toggled.advance(2); + assert.equal(toggled.calls.length, 3, 'switching on resets the deadline'); + assert.equal(toggled.messages.filter(item => item.message.type === 'audioData').length, 3); + toggled.advance(3); + assert.equal(toggled.calls.length, 3, 'subsequent limited tasks respect the new deadline'); + toggled.loop.stop(); + + const failed = loopHarness(EmulationLoop); + failed.adapter.runFrame = () => { throw new Error('emulator failed'); }; + failed.loop.start(); + assert.equal(failed.loop.running, false); + assert.equal(failed.timers.size, 0); + assert.equal(failed.messages[0].message.message, 'emulator failed'); +} + +function testInput(EmulationLoop) { + const shortPress = loopHarness(EmulationLoop); + shortPress.loop.start(); + shortPress.loop.setInput(0, 8); + shortPress.loop.setInput(0, 0); + shortPress.advance(10); + assert.equal(shortPress.calls.length, 1, 'a short press survives waiting for the next frame'); + shortPress.runNext(); + assert.equal(shortPress.calls[1].action, 7); + shortPress.runNext(); + assert.equal(shortPress.calls[2].action, 15, 'the latch clears after the next frame'); + shortPress.loop.setInput(0, 1); + assert.equal(shortPress.loop.inputMasks()[1], 14, 'the first action bit is active-low'); + shortPress.loop.setInput(0, 3); + assert.equal(shortPress.loop.inputMasks()[1], 12, 'held action bits combine'); + shortPress.loop.releaseInputs(); + assert.deepEqual(shortPress.loop.inputMasks(), [15, 15]); + shortPress.loop.stop(); +} + +function testWorklet() { + let Processor; + const context = vm.createContext({ + Float32Array, Math, sampleRate: 48000, + AudioWorkletProcessor: class { constructor() { this.port = {}; } }, + registerProcessor: (_name, value) => { Processor = value; }, + }); + vm.runInContext(fs.readFileSync(path.join(docs, 'audio-worklet.js'), 'utf8'), context); + const processor = new Processor(); + const samples = new Float32Array(1024); + for (let i = 0; i < samples.length; i += 2) { samples[i] = .25; samples[i + 1] = -.25; } + processor.port.onmessage({ data: samples.buffer }); + const outputs = [[new Float32Array(128), new Float32Array(128)]]; + processor.process([], outputs); + assert.equal(outputs[0][0][0], .25); + assert.equal(outputs[0][1][127], -.25, 'stereo channel order is preserved'); + processor.port.onmessage({ data: 'clear' }); + processor.process([], outputs); + assert.ok(outputs[0].every(channel => channel.every(value => value === 0)), 'cleared/empty audio produces silence'); + processor.port.onmessage({ data: new Float32Array(48000).buffer }); + assert.equal(processor.writeCursor - processor.readCursor, 2047, '500ms burst is bounded below 43ms'); + context.sampleRate = 44100; + const resampled = new Processor(); + resampled.port.onmessage({ data: samples.buffer }); + resampled.process([], outputs); + assert.equal(resampled.readCursor, 139, '44.1kHz output resamples 48kHz source duration'); +} + +(async () => { + const source = fs.readFileSync(path.join(docs, 'emulation-loop.js'), 'utf8'); + const {EmulationLoop} = await import('data:text/javascript;base64,' + Buffer.from(source).toString('base64')); + testClock(EmulationLoop); + testInput(EmulationLoop); + testWorklet(); + console.log('Wasm frontend tests passed: frame pacing, input latch and audio buffering.'); +})().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/scripts/wasm_reference.rb b/wasm/test/reference.rb similarity index 63% rename from scripts/wasm_reference.rb rename to wasm/test/reference.rb index 9d4269f..1cfa5a0 100755 --- a/scripts/wasm_reference.rb +++ b/wasm/test/reference.rb @@ -3,9 +3,9 @@ require 'digest' require 'json' -require_relative '../lib/rubyboy/emulator_wasm' +require_relative '../../lib/rubyboy/emulator_wasm' -rom_path = File.expand_path('../lib/roms/tobu.gb', __dir__) +rom_path = File.expand_path('../../lib/roms/tobu.gb', __dir__) rom = File.binread(rom_path).bytes checkpoints = [1, 2, 15, 30, 45, 60] @@ -21,32 +21,20 @@ def input_for(tick) [direction, action] end -def run_reference(rom, mode, checkpoints) +def run_reference(rom, checkpoints) emulator = Rubyboy::EmulatorWasm.new(rom) accumulated_audio = +''.b - frames = 0 results = [] 60.times do |index| tick = index + 1 direction, action = input_for(tick) - budgets = mode == 'frame' ? [] : [32_768, 32_768, 4688] - audio = +''.b - if mode == 'frame' - emulator.step(direction, action) - frames += 1 - audio << emulator.audio_samples.pack('e*') - else - budgets.each do |budget| - frames += emulator.run_cycles(budget, direction, action) - audio << emulator.audio_samples.pack('e*') - end - end + emulator.step(direction, action) + audio = emulator.audio_samples.pack('e*') accumulated_audio << audio next unless checkpoints.include?(tick) results << { tick:, - frames:, video_sha256: Digest::SHA256.hexdigest(emulator.framebuffer.pack('V*')), audio_sha256: Digest::SHA256.hexdigest(audio), audio_bytes: audio.bytesize, @@ -54,7 +42,7 @@ def run_reference(rom, mode, checkpoints) accumulated_audio_bytes: accumulated_audio.bytesize } end - { mode:, checkpoints: results } + results end puts JSON.generate({ @@ -62,6 +50,5 @@ def run_reference(rom, mode, checkpoints) rom_sha256: Digest::SHA256.file(rom_path).hexdigest, ticks: 60, inputs: (1..60).map { |tick| input_for(tick) }, - cycle_budgets: [32_768, 32_768, 4688], - scenarios: %w[frame cycles].map { |mode| run_reference(rom, mode, checkpoints) } + checkpoints: run_reference(rom, checkpoints) }) diff --git a/wasm/test/ruby_wasm.mjs b/wasm/test/ruby_wasm.mjs new file mode 100644 index 0000000..669981d --- /dev/null +++ b/wasm/test/ruby_wasm.mjs @@ -0,0 +1,50 @@ +// Smoke test for the packed ruby.wasm: its /lib files and the shared browser adapter. +// node wasm/test/ruby_wasm.mjs [path/to/rubyboy.wasm] +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { readFileSync } from 'node:fs'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { DefaultRubyVM } from '../../build/browser-runtime/node_modules/@ruby/wasm-wasi/dist/esm/browser.js'; +import { File } from '../../build/browser-runtime/node_modules/@bjorn3/browser_wasi_shim/dist/index.js'; +import { RubyboyVM } from '../../docs/rubyboy-vm.js'; + +const repoRoot = fileURLToPath(new URL('../../', import.meta.url)); +const hash = bytes => createHash('sha256').update(new Uint8Array(bytes)).digest('hex'); +const wasmPath = process.argv[2] || `${repoRoot}docs/rubyboy.wasm`; +const { vm, wasi } = await DefaultRubyVM(await WebAssembly.compile(readFileSync(wasmPath))); +console.log(`Wasm: ${wasmPath} (${vm.eval('RUBY_DESCRIPTION').toString()})`); + +// /lib must hold exactly the tracked files, so a stale pack or local saves fail here. +const tracked = execFileSync('git', ['ls-files', '-z', 'lib'], { cwd: repoRoot, encoding: 'utf8' }) + .split('\0').filter(Boolean).sort(); +vm.eval("require 'digest/sha2'; require 'json'"); +const packed = JSON.parse(vm.eval(`JSON.generate( + Dir.glob('/lib/**/*', File::FNM_DOTMATCH).select { |path| File.file?(path) }.sort + .to_h { |path| [path.delete_prefix('/'), Digest::SHA256.file(path).hexdigest] })`).toString()); +assert.deepEqual(Object.keys(packed), tracked, 'Packed /lib differs from tracked files'); +for (const path of tracked) assert.equal(packed[path], hash(readFileSync(repoRoot + path)), `Stale packed file: ${path}`); + +const root = wasi.fds[3].dir; +const core = new RubyboyVM(vm, root, File); +for (const value of ['#{raise "must not execute"}', '"quoted" \\ path 日本語\nnext line']) { + assert.equal(core.toValue(value).toString(), value, 'Ruby value conversion must preserve literal strings'); +} + +core.loadUploadedRom(Uint8Array.from(readFileSync(`${repoRoot}lib/roms/tobu.gb`)).buffer); +assert.equal(root.contents.has('rom.data'), false); +let audioBytes = 0; +for (let frame = 0; frame < 60; frame++) { + core.runFrame(15, 15); + assert.equal(core.framebuffer().byteLength, 160 * 144 * 4); + audioBytes += core.popAudio()?.byteLength ?? 0; +} +assert.ok(audioBytes > 0, 'The APU must produce audio'); +assert.throws(() => core.loadUploadedRom(new ArrayBuffer(5)), /ROM size/); +assert.throws(() => core.loadPreInstalledRom('../other.gb'), /ROM not found in allowed ROMs/); +for (const name of ['tobu.gb', 'bgbtest.gb']) { + core.loadPreInstalledRom(name); + core.runFrame(15, 15); + assert.equal(core.framebuffer().byteLength, 160 * 144 * 4); +} +console.log(`PASS ${tracked.length} packed files, adapter, ROM loading and 60 frames`); diff --git a/wasm/test/spinel.mjs b/wasm/test/spinel.mjs new file mode 100644 index 0000000..383f682 --- /dev/null +++ b/wasm/test/spinel.mjs @@ -0,0 +1,87 @@ +// Execute both backends through the shared RubyboyVM and compare Ruby outputs. +// node --experimental-wasm-exnref wasm/test/spinel.mjs +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { createHash } from 'node:crypto'; +import { spawnSync } from 'node:child_process'; +import * as shim from '../../build/browser-runtime/node_modules/@bjorn3/browser_wasi_shim/dist/index.js'; +import { createRubyboySpinel } from '../../docs/spinel/rubyboy-spinel.mjs'; +import { RubyboyVM } from '../../docs/rubyboy-vm.js'; + +const hash = bytes => createHash('sha256').update(new Uint8Array(bytes)).digest('hex'); +const native = spawnSync(process.env.RUBY || 'ruby', ['wasm/test/reference.rb'], { + encoding: 'utf8', maxBuffer: 1024 * 1024, +}); +assert.equal(native.status, 0, native.stderr); +const reference = JSON.parse(native.stdout); +const bytes = readFileSync('docs/spinel/rubyboy-spinel.wasm'); +const module = await WebAssembly.compile(bytes); +assert(WebAssembly.Module.imports(module).every(entry => entry.module === 'wasi_snapshot_preview1')); +const { root, executor } = await createRubyboySpinel(module, shim); +const core = new RubyboyVM(null, root, shim.File, { executor, toValue: value => value }); +const roms = root.contents.get('lib').contents.get('roms'); +// Only Git-tracked ROM files are embedded; local saves and states must not be. +const embedded = dir => [...dir.contents].flatMap(([name, entry]) => + entry instanceof shim.File ? [name] : embedded(entry).map(path => `${name}/${path}`)); +const tracked = spawnSync('git', ['ls-files', '-z', 'lib/roms'], { encoding: 'utf8' }).stdout.split('\0').filter(Boolean); +assert.deepEqual(embedded(roms).map(path => `lib/roms/${path}`).sort(), tracked.sort()); +for (const path of tracked) { + let file = root; + for (const part of path.split('/')) file = file.contents.get(part); + assert(file instanceof shim.File && file.readonly); + assert.equal(hash(file.data), hash(readFileSync(path))); +} +const rom = readFileSync('lib/roms/tobu.gb'); +core.loadUploadedRom(Uint8Array.from(rom).buffer); +assert.equal(root.contents.has('rom.data'), false); +assert.equal(core.popAudio(), null); +const accumulated = []; +let audioBytes = 0; +for (let tick = 1; tick <= reference.ticks; tick++) { + const [direction, action] = reference.inputs[tick - 1]; + core.runFrame(direction, action); + const block = core.popAudio(); + const audio = block ? Buffer.from(block) : Buffer.alloc(0); + accumulated.push(audio); + audioBytes += audio.length; + assert.equal(core.popAudio(), null); + const expected = reference.checkpoints.find(entry => entry.tick === tick); + if (!expected) continue; + const file = root.contents.get('video.data'); + const video = core.framebuffer(); + assert.deepEqual({ tick, video_sha256: hash(video), + audio_sha256: hash(audio), audio_bytes: audio.length, + accumulated_audio_sha256: hash(Buffer.concat(accumulated)), + accumulated_audio_bytes: audioBytes }, expected, `frame tick ${tick}`); + const transferred = structuredClone(video, { transfer: [video] }); + assert.equal(video.byteLength, 0); + assert.equal(hash(transferred), hash(file.data)); +} +assert(audioBytes > 0); +console.log(`PASS frame: identical Ruby framebuffer/audio bytes at six checkpoints`); +for (const name of ['tobu.gb', 'bgbtest.gb']) { + core.loadPreInstalledRom(name); + core.runFrame(15, 15); + assert.equal(core.framebuffer().byteLength, 160 * 144 * 4); +} +assert.throws(() => core.loadPreInstalledRom('../other.gb'), /ROM not found in allowed ROMs/); +assert.throws(() => core.loadUploadedRom(new ArrayBuffer(5)), /ROM size/); +const unsupported = Uint8Array.from(rom); +unsupported[0x147] = 0xff; +assert.throws(() => core.loadUploadedRom(unsupported.buffer)); +assert.equal(root.contents.has('rom.data'), false); +core.loadUploadedRom(Uint8Array.from(rom).buffer); +for (let frame = 0; frame < 1500; frame++) { + core.runFrame(15, 15); + assert.equal(core.framebuffer().byteLength, 160 * 144 * 4); + core.popAudio(); +} +const lcdOff = Uint8Array.from(rom.subarray(0, 32768)); +lcdOff[0x147] = lcdOff[0x148] = lcdOff[0x149] = 0; +lcdOff.set([0xc3, 0x50, 0x01], 0x100); +lcdOff.set([0x3e, 0, 0xe0, 0x40, 0x18, 0xfe], 0x150); +core.loadUploadedRom(lcdOff.buffer); +core.runFrame(15, 15); +assert.equal(core.framebuffer().byteLength, 160 * 144 * 4); +assert(core.popAudio().byteLength > 0); +console.log(`PASS embedded files, shared adapter, ROM/error recovery, LCD-off and 1500 frames (${bytes.length.toLocaleString()} bytes)`);