mirror of
https://github.com/st3fan/ewm.git
synced 2026-10-25 02:02:15 +00:00
The closer for the original //e: a `builtin:apple2e` config so the machine E3 added is reachable by name, and the docs to match. This completes plans/20260720-02. - configs/apple2e.json: model apple2e, aux ext80col, UniDisk 3.5 (Liron) in slot 5, Disk ][ in slot 6, RGB — the same layout as apple2enhanced, on the 6502. Registered in BUILTINS (sorted apple2 < apple2e < apple2enhanced); the builtin list / title / model / self-containment tests extend. - Boot gate: builtin_apple2e_boots_dos33 builds the machine through the config pipeline and boots DOS 3.3 to the Applesoft `]` prompt (the //e autostarts the slot 6 disk; DOS 3.3 runs on the 6502). - Docs: README "What's emulated" folds the original and Enhanced //e into one entry and gains a builtin:apple2e profile; notes/JSON_CONFIG.md model inventory + the aux-validity note cover both //e models; IDEAS.md marks "NMOS //e" landed. Gates: cargo fmt / clippy -D warnings / cargo test (365 pass, incl. the new boot gate and readme_examples_parse with the new builtin:apple2e example). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
551 lines
31 KiB
Markdown
551 lines
31 KiB
Markdown
# JSON Machine Configuration (`--config`) — Implementation Plan
|
||
|
||
A working document for configuring a whole machine from one JSON file:
|
||
|
||
```
|
||
ewm two --config myemulator.json
|
||
```
|
||
|
||
In the house style: re-read at the start of every session, update as phases
|
||
land. **Every phase passes the full gates** (`cargo fmt --check`,
|
||
`cargo clippy --all-targets -- -D warnings`, `cargo test`).
|
||
|
||
## Owner's decisions (recorded)
|
||
|
||
- **JSON, not TOML** — better supported (editors, JSON Schema tooling,
|
||
every language). This supersedes the TOML wording in `MAC_APP.md`
|
||
Phase 3.
|
||
- **A JSON Schema** captures *everything* configurable today via the
|
||
command line **and** the Command Palette.
|
||
- **Slots are configurable**: no cards, three Disk ][ controllers, the
|
||
different aux cards — the machine's physical layout lives in the file.
|
||
- **CLI options stay** — only `--config <path>` is added for now.
|
||
|
||
## Status
|
||
|
||
| Phase | Description | Size | Status |
|
||
|---|---|---|---|
|
||
| A | `--config` + schema + serde types; today's layout expressible | M | **Done** |
|
||
| B | Real slot flexibility: any slot, multiple Disk ][ controllers, empty slots | M/L | **Done** |
|
||
| Sources | Built-ins, overlays, `--print-config` (`plans/20260718-02-config-sources.md` C1–C5) | M | **Done** |
|
||
| One family | The same document for `ewm one` (`plans/20260719-02-one-config.md` O1–O5) | M | **Done** |
|
||
| C | "Save current setup" from the palette; `.ewmachine` integration | M | Not started — seeded by `options_to_config` (C4) |
|
||
|
||
## Phase A decisions (recorded as built)
|
||
|
||
- **Precedence is structural, not tracked**: `parse_options` runs two
|
||
passes — pass 1 loads `--config` files into `Options`, pass 2 is the
|
||
unchanged flag loop, which only assigns a field when its flag is present.
|
||
CLI-overrides-config falls out without per-flag "was it given" state.
|
||
Multiple `--config` flags apply in order; config and CLI `--memory`
|
||
regions are additive.
|
||
- **schemars is a dev-dependency only** (owner's decision): the derive is
|
||
gated `#[cfg_attr(test, derive(schemars::JsonSchema))]`, keeping schemars
|
||
out of release builds. Consequence: **no `#[schemars(...)]` attributes on
|
||
the config structs, ever** — doc comments become the schema descriptions.
|
||
Promote it to a plain dependency if an attribute is genuinely needed.
|
||
- **Schema regeneration**: the golden-file test keeps
|
||
`schema/ewm-config.schema.json` byte-for-byte in sync; regenerate with
|
||
`EWM_UPDATE_SCHEMA=1 cargo test -p ewm schema_matches_committed`.
|
||
- **serde caveat resolved**: `deny_unknown_fields` on the internally tagged
|
||
`SlotCard` enum *does* reject typo'd keys with current serde (the
|
||
`unknown_slot_card_key_is_rejected` canary test pins this).
|
||
- **Slot rulings**: slot 7 `"empty"` is accepted (just no HDD attached);
|
||
`"empty"` in slots 1 and 6 is *rejected* with the Phase B message — the
|
||
machine builders hard-wire the Thunderclock and Disk II today, so "no
|
||
card there" is real slot flexibility. *(Superseded by Phase B: any card
|
||
in any slot.)*
|
||
- **Memory addresses are strings**, hex (`"0xd000"`) or decimal
|
||
(`"53248"`); the CLI `--memory` flag stays decimal-only.
|
||
- `cpu.speed` and `input.controller` exist only in the config (no new CLI
|
||
flags), wiring to the palette's speed constants and a preferred-name
|
||
gamepad scan at startup.
|
||
|
||
## Phase B decisions (recorded as built)
|
||
|
||
- **`Two::new_with_slots(model, aux, &BTreeMap<u8, SlotDevice>)`** is the
|
||
table-driven constructor; `Two::new`/`new_with_aux` delegate with the
|
||
default table (Thunderclock@1, Disk II@6), so the whole pre-Phase-B test
|
||
suite runs unchanged. The machine table carries **card kinds only** —
|
||
media inserts afterwards (`load_disk_at(slot, drive, path)`,
|
||
`attach_hdd_at(slot, path)`; the hard drive attaches post-construction
|
||
because `Hdd::new` needs its image up front).
|
||
- **Devices decode only the DEVSEL low nibble** (`addr & 0x0f`); each is
|
||
registered over its slot's 16-byte range, so one implementation serves
|
||
any slot. `DSK_ROM` is naturally slot-agnostic (the P5 ROM derives its
|
||
slot from the return address); the clock and hard-disk firmware are
|
||
generated per slot (`clk_rom(slot)`, `hdd_rom(slot)`), with golden tests
|
||
pinning the slot-1/slot-7 images byte-for-byte to the pre-generator
|
||
statics.
|
||
- **Boot controller = highest slot with a Disk II**; `load_disk`, the
|
||
drag-drop handler, and `dsk()` target it. No config boot hint — the real
|
||
Autostart ROM scan (7→1) picks the boot device, exactly as on hardware
|
||
(proved by the boot-scan tests in `ewm/tests/two_slots.rs`).
|
||
- **Empty slots read `0x00`** (owner's decision), the pre-existing
|
||
unmapped-read behavior on both models — it fails the Autostart boot
|
||
signature just as safely as the floating/`0xFF` this plan originally
|
||
sketched.
|
||
- **Multiplicity**: at most three Disk ][ controllers (the classic
|
||
maximum) and one Thunderclock (ProDOS installs a single clock driver);
|
||
**hard drives are unlimited** (owner's decision) — `Two` keeps them in a
|
||
slot-keyed map like the controllers.
|
||
- **Absent `machine.slots` = the default layout**; a *present* `slots`
|
||
object (even `{}`) is literal — absent keys inside it are empty slots.
|
||
This keeps `{"machine": {"model": "apple2plus"}}` equal to bare `ewm two`
|
||
while honoring "an absent slot key means empty". *(The `--drive1`/
|
||
`--drive2`/`--hdd` sugar this section originally described was replaced
|
||
by `--set`; see the CLI overrides decisions below.)*
|
||
- **Drive lights are OR'ed across controllers** — the status bar and LED
|
||
strip keep their two-light layout; at any moment at most one controller
|
||
spins, so the pair reads as the active controller's drives.
|
||
|
||
## CLI overrides (`--set`) decisions (recorded as built)
|
||
|
||
- **`--set <key>=<value>`** overrides one config value by colon-separated
|
||
key path (`--set machine:slots:6:drive1=game.dsk`, `--set
|
||
display:monitor=amber`). A **separate flag** (owner's decision — not
|
||
overloaded onto `--config`), and the `--drive1`/`--drive2`/`--hdd`
|
||
sugar flags are **removed** (owner's decision); the boo launcher and
|
||
docs speak `--set`.
|
||
- **One config document, sources layered left-to-right**: `--config`
|
||
files load through the typed path (per-file validation and relative-path
|
||
resolution intact) and are serialized back to JSON (`serde::Serialize`
|
||
on the config types — fronting Phase C); `--set` mutates the document
|
||
directly. One typed conversion + validation at the end
|
||
(`config::from_document`). Consequence: multiple `--config` files now
|
||
**deep-merge** instead of a later file's slots table replacing
|
||
wholesale. The remaining convenience flags (`--model`, `--color`, …)
|
||
still override the finished document, per the Phase A precedence design.
|
||
*(Since superseded: the per-setting convenience flags were retired by
|
||
`plans/20260719-01-flag-retirement.md`; `--serve` is the one
|
||
document-overriding flag left.)*
|
||
- **Merge rules** (`config::merge_documents`): objects merge recursively;
|
||
`null` and empty-array overlays are no-ops (a source that doesn't set a
|
||
field must not clear it); two objects whose `"card"` discriminators
|
||
differ replace wholesale (merging a diskii's drives into an `"empty"`
|
||
card would fail validation). `apply_set` mirrors the card rule: setting
|
||
a different `card` resets the object's other fields.
|
||
- **Slots materialization**: a `--set` entering `machine:slots` on a
|
||
document without one materializes the default table first, so `--set
|
||
machine:slots:6:drive1=x` on a bare command line extends the default
|
||
machine exactly like the removed `--drive1` did.
|
||
- **Value typing**: a `--set` value that parses as JSON is used as JSON
|
||
(numbers, booleans, quoted strings, whole objects — `--set
|
||
'machine:slots:7={"card":"harddrive","image":"tr.hdv"}'` is the one-line
|
||
`--hdd` replacement); anything else is a plain string. Escape hatch for
|
||
values that accidentally parse as JSON (a file named `123`): quote them
|
||
(`--set 'machine:slots:6:drive1="123"'`). `--set` path values stay
|
||
as-given (CWD-relative), like the flags they replace; file paths keep
|
||
resolving against their config's directory.
|
||
- **Array paths are rejected** (`machine:memory:0:path`) — memory regions
|
||
come from `--memory`.
|
||
|
||
## Config sources — built-ins (C1, recorded as built)
|
||
|
||
Phase C1 of `plans/20260718-02-config-sources.md`:
|
||
|
||
- **`--config builtin:<name>`** loads one of the embedded copies of the
|
||
`configs/` files (`include_str!`, a static table in `config.rs` — no
|
||
build script). Names are the schema's model tokens, matching the file
|
||
stems 1:1: `builtin:apple2plus` (`configs/apple2plus.json`) and `builtin:apple2enhanced`
|
||
(`configs/apple2enhanced.json`; the files were renamed from `plus.json` /
|
||
`enhanced.json`). `builtin:list` prints the names with descriptions
|
||
and exits 0, like `--help`; an unknown name errors listing the
|
||
available names. A literal file named `builtin:x` is reachable as
|
||
`./builtin:x` (documented, not engineered around).
|
||
- **Built-ins are self-contained**: no drive images, memory files, or
|
||
trace/state paths — there is no directory to resolve relative paths
|
||
against. `load_builtin` rejects file references at load time and the
|
||
`builtins_load_and_are_self_contained` test pins the property (plus:
|
||
every builtin carries a `description`, and the table stays sorted).
|
||
- **Top-level `description`** joined the schema — a one-line human
|
||
description shown by `builtin:list`, usable by any config file.
|
||
- **Bare `ewm two` boots `builtin:apple2plus`** — the default machine
|
||
*is* that built-in config, not an in-code layout (owner's decision;
|
||
the earlier Thunderclock-in-slot-1 in-code default is retired), so
|
||
code and configs describe the ][+ once.
|
||
|
||
## Config sources — partial configs (C2, recorded as built)
|
||
|
||
Phase C2 of `plans/20260718-02-config-sources.md`, the enabling change
|
||
for `--config-overlay` (C3):
|
||
|
||
- **The serde types are partial-friendly**: `Config.machine` and
|
||
`Machine.model` became `Option`, so any fragment — `{}`, a slots-only
|
||
overlay — parses. `merge_documents` needed no change (absent options
|
||
serialize to `null`, already a merge no-op).
|
||
- **Validation split in `config.rs`**: `validate` is *structural* (per
|
||
file, fragment-judgeable: unknown keys, enum values, slot rules,
|
||
multiplicity, sizes/addresses/ports); `validate_complete` is
|
||
*completeness* (final document only: `machine.model` present, plus the
|
||
model cross-checks — aux is //e-only, the //e has no slot 0 — which an
|
||
overlay can't be judged on until the merged document names the model).
|
||
- **Loader contract**: `load`/`load_source_document` (the `--config`
|
||
path) still require completeness per file — a partial file fails with
|
||
`machine.model is required (is this an overlay? use --config-overlay)`
|
||
— while `load_document` runs only the structural pass (with the file
|
||
named in errors and relative paths resolved) and is the path overlays
|
||
will load through. `from_document` runs both passes on the final
|
||
layered document; its missing-model message is
|
||
`machine.model is required (start from --config, e.g. --config
|
||
builtin:apple2plus)`.
|
||
- **Two schemas, one generator** (plan option 2): the generated schema is
|
||
now overlay-shaped, so the golden test post-processes the requiredness
|
||
(`machine`, `machine.model`) back into `schema/ewm-config.schema.json`
|
||
and commits the relaxed one as `schema/ewm-config-overlay.schema.json`
|
||
(own title/description) for editor support of overlay files. One
|
||
regeneration command updates both.
|
||
|
||
## Config sources — overlays (C3, recorded as built)
|
||
|
||
Phase C3 of `plans/20260718-02-config-sources.md`:
|
||
|
||
- **`--config-overlay <source>`** layers a partial config onto the
|
||
document; repeatable, and all sources — the `--config` base, overlays,
|
||
`--set` — apply strictly in command-line order through the existing
|
||
merge. Overlay files load through the structural-only typed path
|
||
(`load_overlay_document` → `load_document`): the overlay file is named
|
||
in errors and its relative paths resolve against its own directory.
|
||
The `builtin:` scheme is shared with `--config` (a complete config is
|
||
a valid overlay); overlay-only command lines start from the default
|
||
machine, like bare `--set`.
|
||
- **One `--config` max** (plan recommendation adopted): a second errors
|
||
with `only one --config allowed; use --config-overlay for additional
|
||
layers`. Mildly breaking — two `--config` files used to deep-merge —
|
||
but that read as an accident once partial layers had their own flag.
|
||
- **Slots materialization extends to overlays**
|
||
(`merge_overlay_document`): an overlay carrying `machine.slots` onto a
|
||
slotless document materializes the default table first, so
|
||
`--config-overlay hdd7.json` means "the default machine plus a hard
|
||
drive in slot 7". A base's explicit table (from `--config`) stays
|
||
literal — materialization fills a missing table, never touches a
|
||
present one. All four base × overlay combinations are pinned in
|
||
`config.rs` tests; `--set`'s materialization is untouched (the boo
|
||
launcher's drag-drop paths behave identically).
|
||
- **Known edge, accepted**: overlaying a complete *//e* config onto the
|
||
slotless ][+ default (`ewm two --config-overlay builtin:apple2enhanced`) fails
|
||
completeness — materialization brings in the ][+ default table, whose
|
||
slot 0 the //e rejects. Consistent with the rules (an overlay means
|
||
"default machine plus this"); start from `--config builtin:apple2enhanced`
|
||
instead.
|
||
|
||
## Config sources — `--print-config` (C4, recorded as built)
|
||
|
||
Phase C4 of `plans/20260718-02-config-sources.md`:
|
||
|
||
- **`ewm two … --print-config`** prints the machine the command line
|
||
describes — sources *and* convenience flags applied, i.e. the machine
|
||
a real run would build — as config JSON on stdout and exits 0. Any
|
||
load/validation error still exits nonzero first, so the flag doubles
|
||
as a config linter for scripts and CI.
|
||
- **`options_to_config` (two.rs) is the one Options→Config mapping** —
|
||
the inverse of `apply_config`, kept as a single function so the
|
||
palette's "save current setup" (Phase C) reuses it. The full mapping
|
||
was implemented; the plan's fallback (print before convenience flags)
|
||
wasn't needed. `--wozbug`, `--break` and the hidden `--screenshot`
|
||
are debug tooling, not machine configuration, and don't appear.
|
||
- **The document is explicit where it matters, quiet where it doesn't**:
|
||
model, slot table, display and cpu settings print even at their
|
||
defaults (stable against future default changes); off-by-default
|
||
extras (strict, debug, boot delay, remote) appear only when enabled.
|
||
`config::compact_document` does the shaping — it drops `null`s, empty
|
||
arrays, and sections that emptied out, but keeps a genuinely bare
|
||
`"slots": {}` (which means "no cards", not "default layout"). Keys
|
||
print in sorted order (documents are sorted maps) — deterministic,
|
||
semantics-free.
|
||
- The printed document round-trips: fed back via `--config` it yields
|
||
identical `Options` (pinned by e2e tests, including a composed
|
||
base + overlay + `--set` + flags command line). Path fields are
|
||
emitted as the run would use them, so `--set` paths given relative to
|
||
the CWD print relative and would re-resolve against the printed
|
||
file's directory — save next to where you ran, or use absolute paths.
|
||
|
||
## Config sources — the one family (recorded as built)
|
||
|
||
`plans/20260719-02-one-config.md` (O1–O5): `ewm one` speaks the same
|
||
configuration language.
|
||
|
||
- **One document type, not a second schema**: `Model` grew `apple1` /
|
||
`replica1` tokens with a derived **family** (`Family::Apple2` vs
|
||
`Family::Apple1`); a config fully describes *which* machine it is —
|
||
the property `.ewmachine` bundles and a future unified launcher need.
|
||
- **Family cross-checks live in the completeness pass**, next to the
|
||
//e-only rules: on one-family documents, `machine.slots`,
|
||
`machine.aux`, `display.*`, `cpu.speed`, `input`, `boot`, `remote`,
|
||
`state`, and `debug.enabled` are **rejected, not ignored** — each
|
||
error names the offending key and model. Valid everywhere:
|
||
`machine.model`, `machine.memory`, `cpu.strict`, `debug.trace`.
|
||
Relaxing a key for the family (e.g. `remote` for REMOTE.md Phase 7)
|
||
is a one-place change.
|
||
- **The subcommands enforce the family both ways** — a one-family
|
||
document is a *valid config* that `two` refuses to run (and vice
|
||
versa), each error pointing at the right subcommand.
|
||
- **The shared source loop** (`config::collect_document`) carries pass 1
|
||
for both subcommands, parameterized by the seed model (`apple2plus` /
|
||
`replica1` — bare `ewm one` boots the Replica 1, the C default) and
|
||
the slots-materialization hook (`two`-only; a one-family document
|
||
never grows the ][+ default table).
|
||
- **Built-ins** `builtin:apple1` / `builtin:replica1` are minimal
|
||
(`description` + `machine.model`; the ROMs are baked into the
|
||
machine); `builtin:list` shows all four names, and the boo bootloader
|
||
boots the Apple 1 / Replica 1 via `--config builtin:…`.
|
||
- **`one`'s flags retired** (`--model`, `--memory`, `--trace`,
|
||
`--strict`) under the same v0.0.1 ruling as
|
||
`plans/20260719-01-flag-retirement.md`; config memory regions are an
|
||
upgrade (hex addresses, per-file relative-path resolution).
|
||
|
||
## Disk images over HTTP (recorded as built)
|
||
|
||
A floppy or hard-drive image path may be an `http(s)://` URL
|
||
(`ewm/src/fetch.rs`):
|
||
|
||
- **Cache**: `$XDG_CACHE_HOME/ewm` (else `~/.cache/ewm`) /
|
||
`<sha1 of the URL>` / `<file name from the URL>`, with a `meta.json`
|
||
holding the validators. The digest keys the directory because URLs
|
||
are not filesystem-safe; the file keeps its own name so the cache is
|
||
browsable and errors read normally. SHA-1 is the crate's own
|
||
(`ws::sha1`) — no new dependency for hashing.
|
||
- **Freshness**: one *conditional* GET (`If-None-Match` /
|
||
`If-Modified-Since` from the stored validators) rather than HEAD then
|
||
GET — a `304` costs a single round trip. If the request fails and a
|
||
copy is cached, the cached copy is used, so a downloaded machine
|
||
still boots offline.
|
||
- **A URL is a source, not a path**: `resolve_paths` leaves it alone
|
||
(like `builtin:`), but `referenced_files` *does* count it, so a
|
||
built-in config still may not carry one — builtins must run offline.
|
||
- **Downloaded hard drives mount memory-only** (`writeback = false`):
|
||
ProDOS writes update the in-memory image and never reach the cached
|
||
file, so revalidating a download cannot destroy them. This needed a
|
||
real change, not just leaving the file handle unopened — an
|
||
unopenable file means *WRITE PROTECTED* in `hdd.rs`, which is a
|
||
different (and still supported) state for a local image.
|
||
- **Dependency**: `ureq` (blocking + rustls) — owner's decision, the
|
||
one place the tiny-dependency budget gives way, because TLS is not
|
||
something to hand-roll. It brings ~25 transitive crates including
|
||
`ring`, so the headless server build now compiles native crypto too.
|
||
|
||
## What is configurable today (the schema inventory)
|
||
|
||
Since `plans/20260719-01-flag-retirement.md`, the CLI column is
|
||
`--set <key>=<value>` (or any config source) for everything — the
|
||
per-setting convenience flags are gone. Family rules (the completeness
|
||
pass rejects the rest by name): `machine.model`, `machine.memory`,
|
||
`cpu.strict`, and `debug.trace` apply everywhere; `machine.cpu` and
|
||
RAM-bank (`size`) regions are **apple1-family only**; everything else
|
||
below is **apple2-family only**.
|
||
|
||
| Source | Setting | Values |
|
||
|---|---|---|
|
||
| config + `--set` | `machine.model` | `apple2`, `apple2e`, `apple2plus`, `apple2enhanced`, `apple1`, `replica1` |
|
||
| config + `--set` | `title` | window bar reads `EWM - <title>`, else plain `EWM`; the built-ins set the machine name |
|
||
| config + `--set` | `machine.cpu` | `6502`, `65C02` (apple1 family; absent = the model's CPU) |
|
||
| config + `--set` | `machine.aux` | `80col`, `ext80col`, `ramworksiii` (+ `size`; //e only) |
|
||
| config + `--set` + palette | `display.monitor` | `green`, `amber`, `white`, `rgb` |
|
||
| config + `--set` + palette | `display.scanlines` | `off`, `light`, `heavy` |
|
||
| config + `--set` | `boot.delay` | seconds |
|
||
| config + `--set` | `display.fps` | display refresh |
|
||
| config / overlay files | `machine.memory` | regions: `type` + `address` + one of `path` (a file or `builtin:<rom>`) or `size` (an empty RAM bank; apple1 family). On the apple1 family the regions describe the whole board. (`--set` cannot reach arrays) |
|
||
| config + `--set` | `debug.trace`, `cpu.strict`, `debug.enabled` | debugging |
|
||
| config + `--set` + palette | `cpu.speed` | 1.023 MHz (normal), 3.58 MHz, 7.16 MHz — the classic accelerator steps |
|
||
| config + `--set` + palette | `input.controller` | picked by name when several are present |
|
||
| config + `--set` | slots 0–7 | card per slot (below); slot 0 is the ][+ language-card socket |
|
||
|
||
## Slot 0 decisions (recorded as built)
|
||
|
||
- **Slot 0 is a `"0"` key in `machine.slots`** (owner's decision —
|
||
hardware-faithful, not a separate field). The ][+ language card was
|
||
hardwired into `Two::new_2plus` before this; now it is a declared card.
|
||
- **Card set restricted**: slot 0 takes only `"language"`, `"saturn128"`
|
||
or `"empty"` — it has no `$Cn00` firmware space, so firmware-bearing
|
||
cards can't work there — and those cards fit nowhere else. The //e
|
||
rejects `"0"` outright (its language card is soldered onto the
|
||
motherboard).
|
||
- **The literal-table rule covers slot 0** — a deliberate breaking
|
||
change, accepted by the owner: a ][+ config whose `slots` table omits
|
||
`"0"` is a stock **48K machine** ($D000–$FFFF motherboard ROM on the
|
||
bus, slot 0's DEVSEL range unmapped). `configs/apple2plus.json` declares the
|
||
card explicitly. The default table (absent `slots`, and the `--set`
|
||
materialization) gains `"0": {"card": "language"}`, so bare command
|
||
lines stay the classic 64K build; `--set machine:slots:0:card=empty`
|
||
is the 48K opt-out.
|
||
- **Machine plumbing**: slot 0 never becomes a `SlotDevice` —
|
||
`build_machine` consumes it as a `two::Slot0` (Language / Saturn128 /
|
||
Empty) passed to `Two::new_with_slots`. `Two::slot0()` reports it and
|
||
WozBug's `SLOTS` shows the slot 0 line on the ][+ (with the selected
|
||
bank for the Saturn). DOS 3.3 boots on the 48K machine (it just skips
|
||
loading Integer BASIC) — regression-tested.
|
||
- **`"saturn128"` — the Saturn Systems 128K RAM Board**
|
||
(`ewm/src/saturn.rs`, from the Saturn Operations Manual ch. 9): eight
|
||
16K banks at $D000–$FFFF, each two 4K banks (A/B) at $D000 plus its
|
||
own 8K. The $C08x A2=0 column is the exact Language Card protocol
|
||
(bank 1 is how DOS/Pascal/VisiCalc see a "16K card"); A2=1 selects the
|
||
16K bank ($C084–7 → 1–4, $C08C–F → 5–8), with read/write/4K state
|
||
persisting across switches. Write-enable follows the LC's read-twice
|
||
rule (the manual's "PEEK or POKE" prose is looser; the LC-compatible
|
||
semantics are what software relies on and what `alc.rs` implements).
|
||
Regression-tested: all eight banks hold independent contents on the
|
||
bus, and DOS 3.3 loads Integer BASIC into bank 1 with INT/FP switching
|
||
both ways.
|
||
- **Future**: an Integer BASIC Firmware card would be a fourth slot 0
|
||
card kind; the Saturn 32K/64K siblings would be a size field or
|
||
variants.
|
||
|
||
## The Liron / UniDisk 3.5 decisions (recorded as built)
|
||
|
||
- **`"liron"`** (`ewm/src/liron.rs`): the UniDisk 3.5 Controller as
|
||
virtual hardware in the `hdd.rs` style — hand-assembled `$Cn00`
|
||
firmware over magic DEVSEL ports, no IWM emulation. Two drives
|
||
(`drive1`/`drive2`), **.2mg only**, ProDOS-order, exactly 800 (400K)
|
||
or 1600 (800K) blocks; the 2IMG locked flag mounts read-only;
|
||
write-back lands at `data_offset + block*512`, header preserved. Any
|
||
slot 1–7, no multiplicity limit; `configs/apple2enhanced.json` carries one
|
||
in slot 5.
|
||
- **SmartPort identity is real**: signature `$Cn07=$00`, ID type at
|
||
`$CnFB`, ProDOS entry via `$CnFF` with the SmartPort dispatch at
|
||
entry+3. The dispatch implements STATUS (device count, per-unit
|
||
status + block count, DIB), READ_BLOCK and WRITE_BLOCK (translated
|
||
onto the ProDOS driver's pump); everything else returns $21. The
|
||
**Enhanced //e Autostart scan boots it** — the boot test proved the
|
||
scan accepts `$Cn07=$00`, so an 800K ProDOS .2mg in the highest
|
||
populated slot boots exactly like a hard drive.
|
||
- **Space economies in the 256-byte firmware** (all safe for this
|
||
media): the third SmartPort block byte is ignored (800K = 1600
|
||
blocks), `$45` is not restored after transfers and `$42-$45` are
|
||
borrowed by the dispatch (ProDOS rebuilds `$42-$47` per call), and
|
||
ProDOS STATUS reports empty drives as 0 blocks rather than an error
|
||
(reads fail with `$2F` where it matters). The boot gap below `$40`
|
||
houses the SmartPort block-call setup.
|
||
|
||
## The schema (draft)
|
||
|
||
Draft 2020-12, committed as **`schema/ewm-config.schema.json`**. Sketch of
|
||
the shape (the committed schema is the authority; field-level `description`s
|
||
throughout so editors show help):
|
||
|
||
```json
|
||
{
|
||
"machine": {
|
||
"model": "apple2enhanced",
|
||
"aux": { "card": "ramworksiii", "size": "1m" },
|
||
"slots": {
|
||
"1": { "card": "thunderclock" },
|
||
"5": { "card": "diskii", "drive1": "work.woz" },
|
||
"6": { "card": "diskii", "drive1": "dos33.dsk", "drive2": "blank.dsk" },
|
||
"7": { "card": "harddrive", "image": "Total Replay v6.0.1.hdv" }
|
||
},
|
||
"memory": [ { "type": "rom", "address": "0xd000", "path": "custom.bin" } ]
|
||
},
|
||
"display": { "monitor": "green", "scanlines": "off", "fps": 30 },
|
||
"cpu": { "speed": "normal", "strict": false },
|
||
"input": { "controller": "Xbox Wireless Controller" },
|
||
"boot": { "delay": 1.5 },
|
||
"debug": { "trace": "trace.txt", "enabled": false }
|
||
}
|
||
```
|
||
|
||
Schema rules:
|
||
|
||
- `machine.model` required; everything else optional with the current
|
||
defaults (an empty `{ "machine": { "model": "apple2plus" } }` is today's bare
|
||
`ewm two`).
|
||
- **`slots`**: object keyed `"1"`–`"7"`; each value is a card object
|
||
discriminated by `"card"`: `"diskii"` (`drive1`/`drive2` image paths,
|
||
both optional), `"harddrive"` (`image`), `"thunderclock"`, `"empty"`.
|
||
When the whole `slots` object is **absent** the machine gets the default
|
||
layout (clock in 1, Disk II in 6); when **present** it is literal — an
|
||
absent slot key means empty, and `"empty"` says it explicitly. Up to
|
||
three `"diskii"` entries, one `"thunderclock"`, any number of
|
||
`"harddrive"` cards (Phase B).
|
||
- `machine.aux`: `{ "card": "80col" | "ext80col" | "ramworksiii", "size":
|
||
"64k".."8m" }` — `size` only valid with `ramworksiii`; whole `aux` object
|
||
only valid with a //e model (`"apple2e"` / `"apple2enhanced"`) (enforced in
|
||
code; the schema documents it).
|
||
- `cpu.speed`: `"normal" | "3.58mhz" | "7.16mhz"` — exactly the palette's
|
||
accelerator steps (`SPEED_NORMAL/FAST/FASTER`).
|
||
- `input.controller`: a preferred game-controller name; hot-plug still
|
||
applies when absent or unmatched.
|
||
- **`additionalProperties: false` everywhere** — a typo'd key is an error,
|
||
not a silent ignore.
|
||
- **Relative paths resolve relative to the config file's directory** — the
|
||
property that makes `.ewmachine` bundles (MAC_APP Phase 3) portable.
|
||
|
||
## Implementation
|
||
|
||
### Dependencies (flagged for the owner)
|
||
|
||
- **`serde` + `serde_json`** for parsing into typed structs with
|
||
`deny_unknown_fields` — the ecosystem standard; this is EWM's first
|
||
general-purpose third-party dependency beyond sdl3/fontdue/chrono.
|
||
- **`schemars`** to *derive* the JSON Schema from the same serde structs —
|
||
one source of truth. A unit test regenerates the schema and asserts it
|
||
matches the committed `schema/ewm-config.schema.json` byte-for-byte
|
||
(regenerate with a documented one-liner when the config grows).
|
||
|
||
### Phase A — `--config`, schema, today's capabilities (M)
|
||
|
||
- `ewm/src/config.rs`: the serde types mirroring the schema + `pub fn
|
||
load(path) -> Result<Config, String>` (parse + semantic validation +
|
||
path resolution against the config's directory).
|
||
- `--config <path>` in `parse_options`: the file populates `Options`
|
||
first; **explicitly given CLI flags override config values** (documented
|
||
precedence — makes quick experiments cheap and keeps `.ewmachine` +
|
||
overrides possible later).
|
||
- Phase A accepts only layouts the emulator can already build: slot 6
|
||
`diskii`, slot 7 `harddrive`, slot 1 `thunderclock`, any aux card —
|
||
i.e. today's machine expressed in slot syntax. Any other layout parses
|
||
fine but errors clearly: *"slot 5 diskii: not supported yet (see
|
||
notes/JSON_CONFIG.md Phase B)"*.
|
||
- `input.controller` + `cpu.speed` wire to the existing palette
|
||
mechanisms at startup.
|
||
- **Tests**: schema/struct agreement (the schemars round-trip); example
|
||
configs parse (a committed `ewm/tests/configs/` set); unknown-key and
|
||
bad-value rejections with useful messages; relative-path resolution; a
|
||
boot gate — a config naming the DOS 3.3 disk boots exactly like
|
||
`--drive1` does; CLI-overrides-config precedence.
|
||
- **Docs**: README `--config` section + an example; `MAC_APP.md` Phase 3
|
||
rewritten TOML→JSON pointing here; IDEAS.md config bullet → planned.
|
||
|
||
### Phase B — real slot flexibility (M/L) — done
|
||
|
||
The machine builders used to hard-wire the card set; `Two::new_*` now
|
||
constructs from the slot table (decisions above):
|
||
|
||
- **Multiple Disk ][ controllers**: `Dsk` instances at `$C080 + slot*16`,
|
||
each with its own P5 boot ROM at `$Cn00` — up to three controllers / six
|
||
drives (the classic maximum). The boot scan order follows the //e/][+
|
||
autostart convention (highest slot first).
|
||
- **Empty slots**: no Thunderclock, no Disk II — the vacated soft-switch
|
||
and ROM ranges read `$00` (not the floating/`$FF` originally sketched;
|
||
see the Phase B decisions).
|
||
- Card moves (Disk II in slot 4, clock in slot 2, …) and multiple hard
|
||
drives fall out of the same table-driven construction.
|
||
- **Tests** (`ewm/tests/two_slots.rs`): two controllers with distinct
|
||
state and disks both readable (bus-level probes at both slots'
|
||
addresses); empty slot 6 reads `$00` and the machine falls through to
|
||
BASIC; boot-scan order; a clock moved to slot 2 (through a full ProDOS
|
||
boot); hard drives in two slots; the whole existing suite green with
|
||
the default table, untouched.
|
||
|
||
### Phase C — round-tripping (M)
|
||
|
||
- Palette: "Save machine configuration…" writes the *current* state
|
||
(monitor, scanlines, speed, mounted disks, aux card) as a valid config
|
||
file — the seed of the `.ewmachine` document (MAC_APP Phase 3).
|
||
- `serde::Serialize` on the same structs; a round-trip test
|
||
(save → load → identical machine).
|
||
|
||
## Risks & open questions
|
||
|
||
- **`.po` in `slots`**: the floppy/hard-drive ambiguity does not exist
|
||
here — the slot's card type says which is meant. (The drag-drop size
|
||
heuristic in `media.rs` stays for pathless opens.)
|
||
- **Boot order with multiple controllers** *(resolved in Phase B)*: the
|
||
Autostart scan (slot 7 → 1) picks the boot device — highest populated
|
||
slot wins, as on hardware. No `boot` hint in the config.
|
||
- **Palette state vs config**: monitor/scanlines/speed change at runtime;
|
||
the config sets the *initial* state only (Phase C's save captures the
|
||
current one).
|
||
- **`one` (Apple 1 / Replica 1)**: out of scope for now — the schema's
|
||
`machine.model` enum leaves room (`"apple1"`, `"replica1"`) if wanted
|
||
later.
|