@fugloy/emu 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE.md +27 -0
- package/README.md +235 -2
- package/package.json +30 -3
- package/src/cpu/z80cpu.js +3350 -0
- package/src/debug/debugger.js +788 -0
- package/src/debug/instr.js +90 -0
- package/src/debug/label-file.js +429 -0
- package/src/debug/state.js +273 -0
- package/src/devices/cassette-tape.js +254 -0
- package/src/devices/cassette.js +263 -0
- package/src/devices/disk-image.js +965 -0
- package/src/devices/fdc-wd1771.js +180 -0
- package/src/devices/fdc-wd1793.js +1065 -0
- package/src/devices/interrupt-latch.js +76 -0
- package/src/devices/key-queue.js +278 -0
- package/src/devices/keyboard.js +176 -0
- package/src/devices/printer.js +44 -0
- package/src/devices/sound.js +53 -0
- package/src/devices/video.js +69 -0
- package/src/index.js +23 -0
- package/src/loaders/bas.js +107 -0
- package/src/loaders/cas-format.js +213 -0
- package/src/loaders/cmd-format.js +88 -0
- package/src/loaders/handoff.js +29 -0
- package/src/loaders/load.js +299 -0
- package/src/machine/drive-select.js +42 -0
- package/src/machine/io.js +513 -0
- package/src/machine/machine.js +333 -0
- package/src/machine/memory.js +290 -0
- package/src/machine/models.js +123 -0
- package/src/machine/rom-hashes.js +21 -0
- package/src/machine/sha256.js +74 -0
- package/src/machine/trs80-system.js +289 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Christopher Schweda
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Notice
|
|
2
|
+
|
|
3
|
+
The files under `src/`, all but `src/index.js`, come from [cschweda/trs80-emulator](https://github.com/cschweda/trs80-emulator) at commit 11612849c7e4dbbe76a45b38116486223fb05a9a (v1.6.0), moved into this layout in emu phase 0, under this license:
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2024 TRS-80 Model III Emulator Contributors
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
```
|
package/README.md
CHANGED
|
@@ -1,3 +1,236 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @fugloy/emu
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Headless, deterministic TRS-80 emulator core (the Model III and, since emu phase 6, the Model I) in plain
|
|
4
|
+
JavaScript, for Node and the browser alike. Emu phase 0 brought in the core of
|
|
5
|
+
[cschweda/trs80-emulator](https://github.com/cschweda/trs80-emulator) (v1.6.0):
|
|
6
|
+
the Z80 CPU (`src/cpu/`), the Model III memory map, I/O ports, and system
|
|
7
|
+
(`src/machine/`), the keyboard matrix, cassette, WD1793 disk controller, disk
|
|
8
|
+
images, and printer (`src/devices/`), the `.CAS` and `.CMD` formats
|
|
9
|
+
(`src/loaders/`), and save states (`src/debug/`). Emu phase 6 added the
|
|
10
|
+
Model I with its Expansion Interface (unreleased; the Model 4 is split off into
|
|
11
|
+
a plan of its own: a Model 4 ROM is now local, from the cpmarchives TRS32 ROM
|
|
12
|
+
set, whose `MODEL4P.ROM` is an emulator's stand-in rather than Tandy's 4P ROM;
|
|
13
|
+
the Model 4 phase is planned next, and its native mode still needs an LS-DOS 6
|
|
14
|
+
or TRSDOS 6 boot disk).
|
|
15
|
+
Phases 0–7 of the spec's eight are done (6 for the Model I, 7 for the Model III): the Z80 core passes ZEXDOC and
|
|
16
|
+
ZEXALL, the `Machine` below runs real programs, `m.debug` writes the label
|
|
17
|
+
and coverage file, and phase 4, the MCP server
|
|
18
|
+
[`@fugloy/mcp`](https://github.com/cschweda/fugloy/blob/main/packages/mcp/README.md),
|
|
19
|
+
drives it for an AI assistant. Version 0.3.0 (first released 2026-10-18; 0.2.0 is the version on npm until it is published)
|
|
20
|
+
(`npm i @fugloy/emu`; or use it from a clone for development; Node 22 or later, or any browser bundler: it has no
|
|
21
|
+
dependencies).
|
|
22
|
+
|
|
23
|
+
The renderer, the WebAudio driver, the turbo throttle, the web UI, and every
|
|
24
|
+
ROM, disk, and program file stayed upstream: rendering and sound belong to the
|
|
25
|
+
workbench, and the caller supplies ROM bytes. Provenance and license:
|
|
26
|
+
[`NOTICE.md`](https://github.com/cschweda/fugloy/blob/main/packages/emu/NOTICE.md).
|
|
27
|
+
|
|
28
|
+
The core never writes to the console. Pass a logger to see what it reports,
|
|
29
|
+
such as an unimplemented opcode or a tape load:
|
|
30
|
+
`new TRS80System({ romData, log: (level, text) => console[level === 'info' ? 'log' : level](text) })`.
|
|
31
|
+
Without one it stays silent, unlike upstream, which printed warnings;
|
|
32
|
+
`system.cpu.unimplementedOpcodes` still records every opcode it hit and does
|
|
33
|
+
not implement.
|
|
34
|
+
|
|
35
|
+
## Using the machine
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
39
|
+
import { Machine, CPU_CLOCK_HZ } from '@fugloy/emu'
|
|
40
|
+
|
|
41
|
+
const m = new Machine({ roms: { level2: readFileSync('model3.rom') } }) // Model III, 48K; you supply the ROM
|
|
42
|
+
const shows = (text) => (mm) => mm.video.text().join('\n').includes(text)
|
|
43
|
+
const second = CPU_CLOCK_HZ // T-states per emulated second
|
|
44
|
+
|
|
45
|
+
m.run({ until: shows('Cass?'), cycles: 5 * second }) // boot; stops at the prompt, or after 5 s
|
|
46
|
+
m.keyboard.type('\n') // ENTER: 1500 baud
|
|
47
|
+
m.run({ until: shows('Memory Size?'), cycles: 5 * second })
|
|
48
|
+
m.keyboard.type('\n') // ENTER at Memory Size?
|
|
49
|
+
m.run({ until: shows('READY'), cycles: 5 * second })
|
|
50
|
+
|
|
51
|
+
m.load.cmd(readFileSync('seadrg01.cmd')) // PC at the program's entry
|
|
52
|
+
m.run({ cycles: 5 * second })
|
|
53
|
+
console.log(m.video.text().join('\n')) // the screen, 16 lines
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
A disk image (JV1, JV3, or DMK, detected from its content) goes in a drive
|
|
57
|
+
with `m.fdc.mount(drive, bytes)`, or through `m.load.dsk(bytes)`, which mounts
|
|
58
|
+
it in drive 0 and resets so the ROM boots from it:
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
m.load.dsk(readFileSync('game.dmk')) // → { format: 'dsk', container: 'dmk', drive: 0, boot: true, blocks: [] }
|
|
62
|
+
m.fdc.drives // [{ drive: 0, container: 'dmk', tracks, sides, sectorsPerTrack, writeProtected }, null, null, null]
|
|
63
|
+
writeFileSync('saved.dmk', m.fdc.export(0)) // the disk as DMK bytes, with what the program wrote to it
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Writes stay in memory; `m.fdc.export(drive)` returns the disk as a DMK file,
|
|
67
|
+
whatever container it was mounted from (read JV1, JV3, and DMK; write DMK). A snapshot carries each disk's bytes and container, so a
|
|
68
|
+
disk the session formatted restores as it was. With a disk loaded through
|
|
69
|
+
`m.load.dsk` the label file lists no load blocks and `m.debug.labelFile()`
|
|
70
|
+
needs a `name`.
|
|
71
|
+
|
|
72
|
+
The WD1793 behind the drives follows the Western Digital FD179X data sheet in
|
|
73
|
+
T-states (spec §5): heads step at the programmed rate, sectors come round at
|
|
74
|
+
300 rpm, and bytes arrive one byte time apart, so a disk loads as slowly as it
|
|
75
|
+
does on a real drive, and a program that misses a byte sees Lost Data.
|
|
76
|
+
Give a disk session room: the ROM reaches the boot sector about a quarter
|
|
77
|
+
second after the reset, a loader reading a game takes seconds (Scarfman's
|
|
78
|
+
title is up about 8 s after the reset), and a sector that is not there costs
|
|
79
|
+
about a second. Check the screen between slices of `run` rather than in an
|
|
80
|
+
`until` callback, which reads it after every instruction.
|
|
81
|
+
|
|
82
|
+
`step`, `run`, the registers, memory, the keyboard (matrix typing and a ROM
|
|
83
|
+
keyboard hook), sound events, the loaders, and the disk drives are described
|
|
84
|
+
in spec §3.
|
|
85
|
+
|
|
86
|
+
### The Model I
|
|
87
|
+
|
|
88
|
+
`model: 1` builds a Model I (unreleased, emu phase 6): the 12K Level II ROM
|
|
89
|
+
(1.2 and 1.3 are recognised; a Model III ROM given for it is refused naming its
|
|
90
|
+
model), 4, 16, 32, or 48K of RAM (`ram`, default 48), and the Expansion
|
|
91
|
+
Interface (`expansion`, default `true`) with its 40 Hz clock in the latch at
|
|
92
|
+
`$37E0`, its WD1771 disk controller at `$37EC`–`$37EF`, and its printer.
|
|
93
|
+
`expansion: false` is a bare keyboard unit: no clock, no disk, `$37E0`–`$37EF`
|
|
94
|
+
read `$FF`. The stock video RAM has no bit 6, so lowercase reads back and shows
|
|
95
|
+
as punctuation and a program's mixed-case title in capitals; `lowercase: true`
|
|
96
|
+
fits the lowercase modification. Its clock is 1,774,080 Hz, `clockHz(1)`
|
|
97
|
+
(`CPU_CLOCK_HZ` stays the Model III's):
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
import { Machine } from '@fugloy/emu'
|
|
101
|
+
|
|
102
|
+
const m1 = new Machine({ model: 1, roms: { level2: readFileSync('level2-model1.rom') } })
|
|
103
|
+
const sec = m1.clockHz // clockHz(1): 1,774,080 T-states an emulated second
|
|
104
|
+
m1.run({ until: shows('SIZE?'), cycles: sec }) // MEMORY SIZE? (MEM SIZE? in Level II 1.3); no Cass?
|
|
105
|
+
m1.keyboard.type('\n')
|
|
106
|
+
m1.run({ until: shows('READY'), cycles: 5 * sec })
|
|
107
|
+
m1.load.cmd(readFileSync('elimin8a.cmd')) // Eliminator's Model I-only build
|
|
108
|
+
m1.run({ cycles: 2 * sec }) // "BY: WAYNE WESTMORELAND & TERRY GILMAN"
|
|
109
|
+
m1.load.dsk(readFileSync('rrgarddb.dmk')) // or a disk: the ROM reads track 0, sector 0 to $4200
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Type at 10 characters a second on the Model I (`m1.keyboard.type(text, {
|
|
113
|
+
cps: 10 })`): the Level II keyboard routine drops every other key at the
|
|
114
|
+
default 20. T-states, `m.sound` stamps included, are the model's own
|
|
115
|
+
(`m.clockHz`). A label file records its model, and `mergeLabelFiles` refuses
|
|
116
|
+
a Model I file with a Model III one. Cassettes play at 500 baud only. Real programs that run on it, and
|
|
117
|
+
what is still unverified against the Model I manual, are in spec §5.
|
|
118
|
+
|
|
119
|
+
## Debugging
|
|
120
|
+
|
|
121
|
+
`m.debug` stops on breakpoints and watches, logs memory hits, records which
|
|
122
|
+
bytes ran, were read, written, jumped to, and called, keeps a bounded trace,
|
|
123
|
+
takes snapshots, and writes the label and coverage file that `@fugloy/disasm`
|
|
124
|
+
reads (`fugloy seadrg01.cmd --labels seadrg01.labels.json`). Continuing the
|
|
125
|
+
example above, just after `m.load.cmd`:
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
import { writeFileSync } from 'node:fs'
|
|
129
|
+
|
|
130
|
+
m.debug.break(0x497f, { once: true }) // Sea Dragon's stub copies the game down, then jumps here
|
|
131
|
+
m.run({ cycles: second }) // → { reason: 'break', id: 1, cycles }
|
|
132
|
+
m.debug.coverage.enable() // from here on: exec, operand, read, write, jump, call
|
|
133
|
+
m.debug.trace.enable({ depth: 10_000 }) // the last 10,000 instructions
|
|
134
|
+
const top = m.debug.watch(0x3c00, 64, { write: true }) // log every write to the screen's top line
|
|
135
|
+
m.run({ cycles: 5 * second })
|
|
136
|
+
m.debug.hits(top) // [{ id: 2, kind: 'write', cycle, pc, addr, value, prev }, …]
|
|
137
|
+
m.debug.callers(0x6212) // [{ from: 18974, kind: 'call', count: 80 }, …]
|
|
138
|
+
m.debug.label(0x6212, 'TONE') // a user label: the listing names the routine TONE
|
|
139
|
+
writeFileSync('seadrg01.labels.json', JSON.stringify(m.debug.labelFile({ name: 'seadrg01.cmd' }), null, 2))
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Coverage can start at the load: the label file's `write` field marks every
|
|
143
|
+
byte a self-relocating program's copy writes, and every loaded byte that
|
|
144
|
+
differs from what the load placed, so the disassembler neither mistakes moved
|
|
145
|
+
code for data nor applies the moved code's coverage to the file's bytes (a
|
|
146
|
+
relocate hint says where it runs).
|
|
147
|
+
Everything on `m.debug` is described in spec §3 and §7, the file in §8 and
|
|
148
|
+
[`docs/label-format.md`](https://github.com/cschweda/fugloy/blob/main/docs/label-format.md).
|
|
149
|
+
`examples/sea-dragon/seadrg01.labels.json` is the file of the scripted session
|
|
150
|
+
in `test/integration/sea-dragon-session.js`; `node
|
|
151
|
+
packages/emu/scripts/sea-dragon-labels.mjs` rewrites it (it needs the ROM).
|
|
152
|
+
|
|
153
|
+
Debugging off costs nothing measurable, and the debugger keeps the §4 target.
|
|
154
|
+
On Sea Dragon, 20 emulated seconds of its title screen
|
|
155
|
+
(`node packages/emu/scripts/benchmark.mjs off|debug`, each mode in its own
|
|
156
|
+
process, on the laptop that built phase 3):
|
|
157
|
+
|
|
158
|
+
| Mode | Speed |
|
|
159
|
+
|---|---|
|
|
160
|
+
| Debugging off | about 42× real time (41.5–43.3× over three runs; phase 2 measured about 43×) |
|
|
161
|
+
| Coverage and a 10,000-deep trace | about 30× real time (29.5–30.7×) |
|
|
162
|
+
|
|
163
|
+
`test/integration/benchmark.test.js` asserts at least 20× for both, but only
|
|
164
|
+
when asked: `FUGLOY_BENCH=1 pnpm exec vitest run
|
|
165
|
+
packages/emu/test/integration/benchmark.test.js`, by itself on a quiet machine
|
|
166
|
+
(`./publish.sh check` runs it that way after the suite). In the full parallel
|
|
167
|
+
suite, beside the other ROM tests, it measured 12–15× against about 40× alone,
|
|
168
|
+
so it is skipped there. On the laptop that built phase 6 it measured 40× (off)
|
|
169
|
+
and 29× (debug); Sea Dragon on the Model I runs at about 48×.
|
|
170
|
+
|
|
171
|
+
- Spec: [`docs/fugloy-emu-spec.md`](https://github.com/cschweda/fugloy/blob/main/docs/fugloy-emu-spec.md) (phases 0–7 in §11)
|
|
172
|
+
- Label and coverage file shared with `mcp` and `disasm`: [`docs/label-format.md`](https://github.com/cschweda/fugloy/blob/main/docs/label-format.md)
|
|
173
|
+
- ROM images are never bundled or committed; `roms/` here is gitignored and
|
|
174
|
+
paths are supplied by the caller.
|
|
175
|
+
|
|
176
|
+
## Differential testing
|
|
177
|
+
|
|
178
|
+
Emu phase 7 runs a program here and in [trsjs](https://trsjs.48k.ca/)
|
|
179
|
+
(Peter Phillips's JavaScript conversion of George Phillips's trs80gp) in
|
|
180
|
+
lockstep, and compares the two after every instruction: PC, every register
|
|
181
|
+
(F bit by bit, R as 7 bits), IFF1/2, IM, the T-states, and every memory
|
|
182
|
+
write, with video RAM at checkpoints and at the end. Keys go into both
|
|
183
|
+
keyboard matrices at the same T-state. trsjs has no license, so it is never
|
|
184
|
+
committed or bundled: `pnpm trsjs:install` fetches it into the gitignored
|
|
185
|
+
`tools/trsjs/`, pinned by SHA-256, and the tests that need it are opt-in.
|
|
186
|
+
|
|
187
|
+
```sh
|
|
188
|
+
pnpm trsjs:install
|
|
189
|
+
FUGLOY_TRSJS=1 pnpm vitest run packages/emu/test/diff # about a minute: the harness tests and six game sessions
|
|
190
|
+
FUGLOY_TRSJS=1 FUGLOY_TRSJS_SWEEP=1 pnpm vitest run packages/emu/test/diff --silent=false # also every other .CMD in examples/ and fixtures/real/, logging each one's class counts and the sum
|
|
191
|
+
pnpm diff examples/sea-dragon/seadrg01.cmd --instructions 10000000 # one program, with a report
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`pnpm diff <program> [--keys <script.json>] [--instructions N] [--from-cycle C] [--strict | --strict-all]`
|
|
195
|
+
boots the Model III ROM from `fugloy.mcp.json`, loads the `.CMD` or `.BAS`,
|
|
196
|
+
and prints the count of each known difference and the first divergence, if
|
|
197
|
+
any, with ten instructions disassembled on each side. Exit status 0 means no
|
|
198
|
+
divergence, 1 a divergence, 2 a usage or setup error (an unexpected error,
|
|
199
|
+
such as the ROM not reaching READY or trsjs throwing, counts as one).
|
|
200
|
+
|
|
201
|
+
Where the two differ, trsjs is the inaccurate side, and the harness says so
|
|
202
|
+
explicitly. Some differences it corrects in the reference (a write to ROM or
|
|
203
|
+
the keyboard, a byte stored wider than 8 bits, PC running past `$FFFF`, a
|
|
204
|
+
repeating CPDR's T-states, a DD or FD prefix trsjs skips); the rest are known classes in
|
|
205
|
+
`test/diff/known.js`, each with the evidence for it, counted rather than
|
|
206
|
+
reported (flag bits 3 and 5; BIT's S and P/V; R on an interrupt and on
|
|
207
|
+
DDCB/FDCB; the HALT return address; carry after CPI/CPD/CPIR/CPDR; and three
|
|
208
|
+
interrupt timings). Everything else is a divergence. On the six sessions
|
|
209
|
+
(Sea Dragon, Eliminator c and e, Rear Guard 5 and 4, Armored Patrol, 10
|
|
210
|
+
million instructions each) there is none. The harness found two bugs on our
|
|
211
|
+
side, both now fixed: a halted CPU did not count R, and port `FF` did not
|
|
212
|
+
read port `EC`'s bits back. After those fixes, every remaining difference is
|
|
213
|
+
trsjs's. The harness cannot see a bug both machines share: where the two
|
|
214
|
+
agree, both may be wrong, so behaviour such as EI's one-instruction delay is
|
|
215
|
+
pinned by our own unit tests (against Zilog UM0080 and Young §5.5) as well as
|
|
216
|
+
compared. One is a judgment call rather than a trsjs error: port `FF` bit 6,
|
|
217
|
+
which the Model III Technical Reference Manual calls undefined; we read it as
|
|
218
|
+
1, as trsjs and trs80gp do. Details in spec §10 and §11 (phase 7). The Model I waits
|
|
219
|
+
for a Model I reference; trsjs is Model III only.
|
|
220
|
+
|
|
221
|
+
## Testing the Z80 core
|
|
222
|
+
|
|
223
|
+
ZEXDOC and ZEXALL, Frank Cringle's Z80 instruction exercisers, are GPL-2.0, so
|
|
224
|
+
they are never committed. `pnpm zex:install` downloads them into the
|
|
225
|
+
gitignored `tools/zex/`, pinned to one commit of
|
|
226
|
+
[agn453/ZEXALL](https://github.com/agn453/ZEXALL) and checked by SHA-256. Then:
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
FUGLOY_ZEX=1 pnpm vitest run packages/emu/test/exerciser # about 5 minutes, one test per group
|
|
230
|
+
FUGLOY_ZEX=1 pnpm vitest run packages/emu/test/exerciser -t "daa,cpl" # one group
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
They run on `test/cpm.js`, a CP/M stand-in: the program at `0100H`, BDOS
|
|
234
|
+
functions 2 and 9 at `0005H`, and a jump to `0000H` to end.
|
|
235
|
+
`test/unit/cpu-timing.test.js` checks every opcode's T-states against
|
|
236
|
+
`@fugloy/z80`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fugloy/emu",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Headless, deterministic TRS-80 emulator core, Model III today (see docs/fugloy-emu-spec.md)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"NOTICE.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@fugloy/disasm": "1.2.0",
|
|
19
|
+
"@fugloy/z80": "1.2.0"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/cschweda/fugloy.git",
|
|
24
|
+
"directory": "packages/emu"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/cschweda/fugloy#readme",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/cschweda/fugloy/issues"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
6
33
|
}
|