Skip to content

Magic Desk Plus cartridge support (#727) - #844

Draft
enver-haase wants to merge 10 commits into
GideonZ:masterfrom
enver-haase:feat/magic-desk-plus
Draft

enver-haase wants to merge 10 commits into
GideonZ:masterfrom
enver-haase:feat/magic-desk-plus

Conversation

@enver-haase

@enver-haase enver-haase commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Status: draft, waiting for decisions from @GideonZ (1 and 2 under Open) and for VICE's CRT V3 (3). Then from me: the store in V3 form, and the E2E run repeated on a U2+L. Merged with master on 2 October, no conflict left. The mapper itself works on hardware.

Adds Magic Desk Plus (#727), the format of Murder on the Mississippi Remastered.

What it does

  • Gateware (all_carts_v5.vhd): DE00 selects 128 banks of 8K (bit 7 switches the ROM off), DE01 is a 256-byte page register, DE03 picks the SRAM half (bit 0) and SRAM or EEPROM (bit 5). The DF00 window stays served with the ROM off.
  • Loader (c64_crt.cc): CRT type 87, as VICE assigns it. The subtype byte (0-4, VICE r46239) says which parts are fitted and sets the EEPROM page mask; any other subtype is refused.
  • Memory: 128K SRAM plus 8K or 32K EEPROM, in the region the REU and GeoRAM share, so the cart prohibits the REU and everything else on the IO pages.

Open

  1. Memory region: keep the shared region, or give the cart its own area above cartridge RAM on U64/U64-II and drop the U2, where that address is cartridge ROM.
  2. g_max_cart_bits must be at least 21 for 128 banks. The U2+ top levels pass 22, the defaults are 20, and the U64-family top levels are not in this repository.
  3. Store in the CRT: it will be saved into the CRT, as the 1541 Ultimate already does for GMod2, in the form CRT V3 defines once the VICE team has settled it (working draft, posted here). The branch's private $DF00 packets will be replaced by that.

Tests

result
tb_magic_desk_plus.vhd, simulation, head 339efcf8 all checks pass; 17 fail against master's all_carts_v5.vhd
loader host tests, software/io/c64/tests, head 339efcf8 629 checks, 0 failed
firmware, make u64ii, head 339efcf8 builds, no new warning, app_space PASS
E2E magicdesk-plus-cartridge, U2+L in a C64 Ultimate, CI build 13 checks OK; before the ROM-off fix, checks 11 and 12 failed
crystalct's Magic Desk Plus Manager, from the release SRAM and 32 KB EEPROM detected, both diagnostics pass

The two hardware runs were on 18 September (E2E on 727d3129, Manager on a2124a8f), before the branch was rebased onto a newer master; not repeated on the current head.

Not tested

The U64 family: its bitstreams are built outside this repository. The U2+L bitstream comes from CI.

@enver-haase

Copy link
Copy Markdown
Contributor Author

Corrected the EEPROM handling: VICE does say which part is fitted, and I had claimed it does not.

magicdesk_eeprom_open_image() takes the size from the image file and accepts exactly two:

  if (size != 8192 && size != 32768) {
    log_message(LOG_DEFAULT,
                "MAGICDESK: Invalid EEPROM image size (must be 8K or 32K).");
    return -1;
  }

and the page mask follows from it, in both io2 handlers: page_mask = (magicdesk_eeprom.size == 8192) ? 0x1f : 0x7f. Those are the two masks this branch already implements in the cartridge logic, so only the software side was wrong.

It now does the same. The store travels as chunks at DF00 and the size says which is which — 8K or 32K is the EEPROM, 128K is the SRAM, and the three cannot be confused. A size that is none of them is refused rather than loaded. The variant, and with it the mask, follows the EEPROM image size instead of being fixed.

One consequence worth naming: a cart that brings only SRAM now gets the 8K mask, not the 32K one I had picked. That is what VICE creates when it has to make an EEPROM image from nothing, so it is the more faithful default.

Rebuilt: make u64ii_no_esp from a cleaned tree, exit 0, no new warning. The cartridge logic and its testbench are untouched by this — the masks were right there already.

Everything in the opening paragraph still stands: none of this has run on hardware, and Murder on the Mississippi Remastered has not been tried.

@enver-haase

Copy link
Copy Markdown
Contributor Author

Adversarial pass over my own branch. Three findings, one of them fatal. Two are fixed; the third turned out not to be mine to fix.

Fatal: a 128K chunk cannot exist in a CRT

The size field of a CHIP header is 16 bits — uint16_t size = get_word(chip_header + CRTCHP_SIZE). My MDPLUS_SRAM_SIZE was 0x20000, so the comparison could never be true, and save_crt would have written fo->write(cc->ram_location, size, ...) with a truncated length. 64K does not fit either.

Neither the simulation nor the build could have caught this: the testbench exercises the cartridge logic, not the file parser, and a constant that never matches compiles happily.

Fixed by using the bank field, which a DF00 chunk does not otherwise need: bank 0 is the EEPROM at 8K or 32K, banks 1 to 4 are the SRAM in quarters of 32K, in address order. That also removes an ambiguity I had built in, where 32K would have meant both "EEPROM" and "a quarter of the SRAM". A chunk of any other shape is now refused instead of loaded.

Real: the prohibition missed the DF00 page

I had CART_PROHIBIT_DEXX | CART_REU. But

#define CART_PROHIBIT_DFXX (CART_ACIA_DF | CART_REU | CART_MAXREU | CART_UCI | CART_SAMPLER)

so the UCI at DF1C — inside the window — the sampler, an ACIA at DF00 and CART_MAXREU were all left enabled. This cart owns DE00..DE03 and the whole DF00 page. Fixed: CART_PROHIBIT_IO.

Not a defect: saving is explicit here, by design

I was going to add an automatic write-back, on the grounds that "battery-backed" means the player should not lose their notebook. Checking first was the right call: nothing on this device persists cartridge state by itself. save_crt() has exactly one caller, the "Save Cartridge" menu action, sitting beside "Save REU" and "Save MP3". EasyFlash's flash writes and GMod2's EEPROM go the same way.

So this is a house convention, not a gap, and Magic Desk Plus follows it rather than becoming the one cart that saves on its own. The consequence should be said plainly, because this cart is unusual in how much it depends on it: the reason Murder on the Mississippi Remastered uses this format at all is that it writes notes and progress continuously. On real hardware a battery keeps them. Here they live in the shared memory until the player picks Save Cartridge, and a power cycle without that loses them.

If that trade should come out differently for this cart, it is a UX call rather than a technical one, and it is yours.

Unchanged

The cartridge logic and its testbench are untouched by all three: the address map, the register decode and the 0x1F/0x7F masks were right from the start. REU_MEMORY_BASE, __reu_ram_start and g_georam_base are all 0x1000000, so the software and the gateware do mean the same memory.

One minor note carried over from #822: the store is filled with a plain memcpy through a cached address, while the AX88772 driver reaches its DMA buffers through BIT31. No effect today, since no shipping target defines DATACACHE.

Rebuilt after both fixes: make u64ii_no_esp from a cleaned tree, exit 0, no new warning.

Still true, and still the most important line here: none of this has run. No bitstream, no device, and the game has not been tried.

@chrisgleissner chrisgleissner added 3.16 Targeted for release 3.16. enhancement New functionality or an improvement to existing behavior, including formats and developer tooling. labels Sep 9, 2026
@chrisgleissner
chrisgleissner changed the base branch from test-merge to master September 13, 2026 12:42

@chrisgleissner chrisgleissner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: Changes Requested

Thank you for the detailed implementation and GHDL simulation testbench (tb_magic_desk_plus.vhd), @enver-haase.

This PR is not yet ready for merge. As noted in the description, this implementation has only been run in VHDL simulation and has not been verified on target firmware/hardware or against real cartridges/games.

Before this PR can be considered for merge, it must be enriched with an automated end-to-end Python test suite in tests/e2e/io/c64/ (and registered in run-tests and tests/README.md), following the established pattern of other recent cartridge fixes in this repository.

Recent Cartridge E2E Test References

Please consult these recent cartridge E2E tests for reference on how synthetic CRT generation and host-side 6502 test routines are structured:

  1. tests/e2e/io/c64/c64gs_cartridge_test.py (commit 7d9bf4bd):
    Generates a synthetic 64-bank Type 15 CRT, uploads it via device.runners.upload("run_crt", ...), copies a 6502 test routine to $C000, and verifies bank switching via IO1 accesses with marker validation in screen RAM ($0400).
  2. tests/e2e/io/c64/comal80_cartridge_test.py (PR #899 / commit c7464da2):
    Builds a synthetic Comal 80 CRT to test bank selection and cartridge-off bit handling.
  3. tests/e2e/io/c64/ocean_cartridge_test.py (commit 2db13b9f):
    Tests Ocean 16K bank switching using synthetic CRT generation.
  4. tests/e2e/io/c64/ultimax_cartridge_test.py (PR #882 / commit fd756470):
    Verifies Ultimax CRT loading and VIC stream output.

Required Changes Before Merge

  1. Add E2E Test Suite (tests/e2e/io/c64/magicdesk_plus_cartridge_test.py):

    • Construct a synthetic Type 19 Magic Desk Plus CRT containing ROM bank chunks, an EEPROM chunk (bank 0 at $DF00), and SRAM chunks (banks 1..4 at $DF00).
    • Run a 6502 test routine from RAM ($C000) to validate:
      • DE00: Bank selection across bits 0..6 (128 banks) and bit 7 ROM disable (exrom_n).
      • DE01 & DF00 window: Page selection via DE01 and window read/write data correctness at $DF00..$DFFF.
      • DE03: EEPROM select (bit 5 = 0) vs SRAM select (bit 5 = 1) and SRAM portion select (bit 0).
      • Window persistence: Confirming that $DF00..$DFFF remains served and writable even when the ROM is switched off (DE00 bit 7 set).
    • Register the new test suite in run-tests and tests/README.md.
  2. Verify FPGA Generics:

    • Double-check that top-level FPGA generics on target platforms set g_max_cart_bits >= 21 so that all 128 banks of 8K are addressable.

Please update the PR with the requested E2E tests once verified on target hardware/firmware.

@enver-haase

enver-haase commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor Author

Please update the PR with the requested E2E tests once verified on target hardware/firmware.

Is this not for @GideonZ to provide the (testable) gateware?

@crystalct

Copy link
Copy Markdown

In the latest update, the Vice Team introduced support for the Magic Desk Plus but strangely decided to assign it a new, additional ID—different from 19. The new ID is 87.

@GideonZ

GideonZ commented Sep 14, 2026

Copy link
Copy Markdown
Owner

I assigned TwoMegabyter to 87.. hehe

@GideonZ

GideonZ commented Sep 14, 2026 •

Copy link
Copy Markdown
Owner

I saw the FPGA changes. They are quite significant. And, I am wondering why the cartridge RAM area was not used. I don't think cartridges should use the REU ram. Did you have a special reason for using REU ram? @enver-haase

@chrisgleissner

chrisgleissner commented Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator

@enver-haase I was referring to your initial statement: “This has never run. Not on hardware, not as a bitstream, not against the game.”

We normally require tests before merging a PR, ideally E2E tests. If testing depends on Gideon providing a bitstream, could you make that dependency explicit in the PR and clarify which tests remain outstanding and who would run them? It would also help to distinguish what the existing simulation tests establish from what still needs hardware verification. Keeping the PR in draft until that verification is complete would make its status clearer.

By the way, were you able to get your C64U repaired? You opened several other PRs a few weeks ago with similar notes about missing hardware testing. Once your setup is working and the necessary builds are available, could you revisit those PRs, test them on your hardware, and update them with the results and any fixes needed? It's always a good idea for us firmware devs to test on our own rig first and publish red/green results.

@enver-haase

Copy link
Copy Markdown
Contributor Author

@GideonZ — fair question, and the reasoning belongs here in the thread rather than
only in the commit message. It comes down to size.

Magic Desk Plus carries 128K of battery-backed SRAM plus an 8K or 32K EEPROM, so
160K in the largest case. The cartridge RAM area is 64K on every target:

target cart RAM cart ROM
u2 0x0EF0000–0x0F00000 (64K) 0x0F00000–0x1000000
u64 0x00EF0000–0x00F00000 (64K) 0x03C00000–0x04000000
u64ii 0x00EF0000–0x00F00000 (64K) 0x03C00000–0x04000000

So the SRAM does not fit in the cartridge RAM area, not even at half its size. The
obvious place to grow into is the megabyte directly above it, 0x0F00000–0x1000000,
which is free on u64 and u64ii and sits right below REU_MEMORY_BASE. On the u2 that
same address is where the cartridge ROM starts. Taking it would make Magic Desk Plus a
U64-family cartridge.

That is what sent me to the memory the REU and GeoRAM share: it is the only region of
that size present on all targets, and the price — this cart and the REU cannot be on at
the same time — is one the code already knows how to charge, via the prohibit mechanism
GeoRAM uses.

I share your instinct that cartridges should not be reaching into REU memory. Two ways
out, and the choice is yours, not mine:

  1. Give the cart its own area above the cartridge RAM on u64/u64ii and declare Magic
    Desk Plus unsupported on the u2. Cheap change — the base address moves, the prohibit
    flag drops, everything else stays.
  2. Keep the shared region and accept the mutual exclusion, as GeoRAM does today.

Tell me which and I will move the branch to it.

One more thing worth deciding while this is open: @crystalct points out VICE gave Magic
Desk Plus its own CRT id 87 rather than extending 19, and you have assigned 87 to
TwoMegabyter here. As it stands this PR detects a Plus the way the VICE patch does — a
type 19 image that brought non-volatile memory with it — so a CRT written by the new VICE
will not be recognised at all. Whether the loader should also accept 87, and how that
squares with TwoMegabyter, is a call I cannot make.


@chrisgleissner — yes, the C64U is working again as of today, and I have set this PR to
draft as you asked. Let me be precise about what stands behind it and what does not.

Verified, and only this: tb_magic_desk_plus drives the registers the way the machine
does and checks the addresses the slot logic produces — 20 checks pass against this branch,
17 of them fail against the unchanged file. make u64ii_no_esp builds clean. That is
simulation of the cartridge logic plus a compile, nothing more.

Not verified, and not verifiable by me: anything on real hardware. There is no synthesis
toolchain here, so the gateware change cannot become a bitstream without @GideonZ. Until
that bitstream exists there is no firmware to flash and no device behaviour to measure,
which makes a red/green E2E for this PR impossible rather than merely outstanding. Once a
bitstream exists I will run Murder on the Mississippi Remastered against it on my own
machine and post what it does, pass or fail.

@enver-haase
enver-haase marked this pull request as draft September 16, 2026 01:13
@chrisgleissner

Copy link
Copy Markdown
Collaborator

@enver-haase Congrats on fixing the C64U! Did you end up soldering your own JTAG adapter based on an FT232H and did you use the unbrick tool in this repo?

@crystalct

Copy link
Copy Markdown

Ultimately, it is Team Vice that defines the CRT IDs, and everyone else using that format falls into line; otherwise, they would create ambiguities and CRTs that are incompatible depending on the platform used. When the TwoMegabyter type becomes "official," it will get its own ID—specifically, the first one available at that time.

@enver-haase

Copy link
Copy Markdown
Contributor Author

@chrisgleissner — bought, not built: an FT232H breakout of the kind recovery/u64ii/README.md suggests. The only soldering was the JTAG pin headers, which that same README lists under "Hardware needed" — so this was the documented path rather than an improvisation.

And yes, the recovery tooling in recovery/u64ii/ is what brought it back.

The machine has been in steady use since: everything measured in #843 and #915 today was measured on it.

@enver-haase

Copy link
Copy Markdown
Contributor Author

@crystalct — this PR adds Magic Desk Plus, and #727 asks for it because of your
Murder on the Mississippi Remastered. It has only run in simulation so far; a
1541 Ultimate-II+L build is next. Do you have a small image that exercises the
mapper and may be shared publicly, for a reproducible test? And does the release
put its EEPROM and SRAM in $DF00 chunks with the bank field numbering them, or
did you lay it out differently? On hardware the Ultimate writes the store back
into the CRT file on "Save Cartridge", so notebook entries should survive a
reload — that round trip is what I mean to measure.

@crystalct

Copy link
Copy Markdown

@enver-haase sure, go to https://github.com/crystalct/MagicDeskPlus/tree/main/CRT_TEST and use (new) mdplustest.crt

enver-haase added a commit to enver-haase/1541ultimate that referenced this pull request Sep 18, 2026
… tested

The loader tests build their own headers, so they confirm the loader agrees
with this file's idea of a CRT rather than with a real one. crystalct offered
mdplustest.crt in GideonZ#844 as the image to test the mapper with: 262720 bytes of
32 ROM banks at $8000 and nothing else, no $DF00 chunk and no store. That is
the released shape, since VICE keeps the SRAM and the EEPROM in files beside
the image.

So take its header byte for byte and load that shape at both 1 MB and 4 MB,
the second because 1 MB is what a cartridge region is unless a target asks for
more and no Magic Desk Plus case covered it. The bank payloads stay this
file's own: what is regressed is the shape and the header, not the manager's
6502.

What it catches that the built headers do not: a storeless cart taking the 32K
page mask instead of the 8K one. VICE masks to $1F when it has to make an
EEPROM image from nothing, and the manager in this very image displays that
choice as "EEPROM 8KB: DETECTED".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@chrisgleissner

Copy link
Copy Markdown
Collaborator

@enver-haase Nice! I also got an Adafruit FT232H months ago but never got around to soldering the wires to it. Hopefully soon. I am glad it worked for you.

@chrisgleissner

chrisgleissner commented Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator

@enver-haase Please note that now that we have an actual CRT, you can either download that at runtime from an E2E or - better - create a synthetic cart that simulates its structure. I have used the latter approach in other E2E tests that you find in master. Having automated tests prevents regressions in future, and we had quite a lot of them in the past few months. Each time we encountered one, a new automated test was added.

@mrdudz

mrdudz commented Sep 18, 2026 •

Copy link
Copy Markdown

Speaking of embedded EEPROM data in the CRT, here is our current working draft: https://pastebin.com/hzaT1GPk

i strongly recommend not to invent your own extensions, that will break things

@enver-haase

enver-haase commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor Author

The mapper now runs on hardware, and the run found a bug in it, which is fixed

This PR opened saying the mapper had only ever been simulated. That is no longer true. It has been measured on an Ultimate II+L plugged into a C64 Ultimate, the former flashed with a CI build of this branch.

First run, on d8a5923b:

[01] the generated Magic Desk Plus cartridge starts and runs its routine  OK
[02] Magic Desk Plus bank selections reach all 128 banks                  OK
[03] the $DF00 window reaches the EEPROM at page $05                      OK
[04] the $DF00 window reaches the EEPROM at page $85, masked to $05       OK
[05..08] the window reaches both SRAM halves at pages $05 and $85         OK
[09] a byte written into the window reads back                           OK
[10] the same page of the other SRAM half is untouched                   OK
[11] the window still reads with the ROM off                           FAIL   probe 2 read $FF, want $05
[12] the window still takes a write with the ROM off                   FAIL   probe 3 read $FF, want $5A
[13] $9FF0 reads the RAM under the cartridge with the ROM off            OK

Three attempts, identical each time. Everything the plain Magic Desk cannot do worked -- all 128 banks, the page register masking $85 to $05, both SRAM halves, write isolation -- but the window stopped answering as soon as $DE00 bit 7 switched the ROM off.

The cause. serve_enable is built from cart_en (all_carts_v5.vhd:170), and this cartridge cleared cart_en to disable the ROM, which took IO2 down with it. The file's own comment says the window stays served either way; the gateware did not do it.

The ROM has to go by a different route. slot_slave decodes $8000-$BFFF on serve_rom alone, without consulting ROMLn, so dropping serve_rom hands that range back to the RAM under the cartridge -- which is what check [13] measures, and why that check constrains the shape of the fix rather than being incidental to it.

-                serve_rom <= '1';
+                serve_rom <= not mode_bits(0);
                 serve_io2 <= '1';
-                cart_en   <= not mode_bits(0);
+                cart_en   <= '1';

Run after the fix, on 727d3129:

magicdesk_plus_cartridge_test: OK (13 checks, 38.6s)

[11] and [12] flipped, [13] held.

tb_magic_desk_plus did not catch this and could not: it drives the cartridge logic and checks the addresses it produces, while serve_enable gates a layer above it. That is the gap between simulation and hardware in this PR, now closed.

One visible consequence: cart_active follows cart_en and drives CART_LEDn, so the cartridge LED now stays lit while a program has the ROM switched off. The cartridge is still serving its window, so that reads as the more truthful of the two, but it is a change to an unrelated indicator and worth someone disagreeing with if they want to.

Testing notes

@chrisgleissner -- on your request for red/green: the suite is green above, and reproducible with

U64_HOST=<cartridge>@<computer> ./run-tests -s magicdesk-plus-cartridge --assume-fix magic-desk-plus-mapper -t 180

Two things from getting there that are worth knowing for any cartridge suite. The cartridge@computer target form is required on a II+L: the unwind keys have to go to a machine that accepts input injection, and the cartridge itself answers machine:input with HTTP 501. Passing one host for both wedges the UI-state gate exactly as ui_state.py's own docstring warns it will. And the default 30 s timeout is not enough for the suite's 1 MB synthetic CRT over WiFi; -t 180 covers it.

Also, MAGIC_DESK_PLUS_MAPPER in tests/lib/machine.py lists U2 as lacking the mapper, and it has to stay listed: this is gateware, no shipped image has it, and the II+L above only has it because it is running a CI build of this branch. --assume-fix remains the right way to run the suite until this merges and a release carries the rebuilt FPGA -- at which point the entry becomes deletable.

@enver-haase

Copy link
Copy Markdown
Contributor Author

What the VICE team has settled

@crystalct was right that the CRT ids are not ours to assign. The format's living specification is VICE's doc/vice.texi, based on Peter Schepers' CRT.txt rev 1.14, for a format that began in CCS64; the VICE team maintains it. I took the open questions there rather than guess, and gpz has answered both.

The subtype byte decides the variant, and the numbering is 0-4

I reported that cartconv, the emulator and the manual gave header byte $1A three different meanings (bug 2257). gpz closed it fixed in r46239:

You missed some more places in the manual :) I missed them, we forgot about the fact there are two different EEPROM options, and it was quickly hacked in (by me, not Claus) before i merged. Oh well. check r46239 (The Emulator was right, ie subtypes 0-4 exist)

So magicdeskplus.h was authoritative all along:

subtype fitted
0 SRAM + 32KiB EEPROM
1 SRAM + 8KiB EEPROM
2 32KiB EEPROM
3 8KiB EEPROM
4 SRAM only

This branch was reading the EEPROM chunk instead, which gets a released image wrong: a release carries no store at all and still has both parts, so the absent chunk was read as 8K and the page register masked to $1F where the hardware masks it to $7F. All three language builds of Murder on the Mississippi Remastered came out with the wrong mask. Fixed in a2124a8f: the variant now comes from the revision byte, a revision the format does not define is refused, and the loader test states the contract the other way round -- the rows declare a revision and expect a mask, and one row carries a 32K chunk under revision 1 so that a return to reading the chunk cannot pass.

The store does not belong in CHIP packets, and a format revision is coming

Section 87 currently says "SRAM and EEPROM data are stored in separate optional image files, not as CHIP packets in the CRT image". This branch does the opposite: it carries the store in $DF00 CHIP packets numbered by the bank field. I checked what the emulator makes of such a file -- it attaches and banks correctly, but the packets are ignored; a byte planted in an EEPROM packet reads back as $FF.

gpz, on the same ticket:

Mind you, we are working on the next revision of the CRT format right now, which will finally allow to include extra data like that cleanly. I strongly recommend not to use your own extensions for this, because it will result in CRT files that will no more work in VICE (and perhaps other emulators that use the specs)

@GideonZ -- that is the second half of the question you raised on 14 September, answered by the people who own the format rather than by me. A CRT this device writes with $DF00 store packets is a private extension, and the format's maintainers are asking us not to create one while the replacement is being drafted.

I would hold the store format until that revision lands, and have "Save Cartridge" write nothing rather than write something we have been asked not to write. That leaves the memory-region question you actually asked still open and independent of this -- none of the mapper work depends on either answer.

What the release actually is

For the record, since much of this thread has been inference. All three language builds of Murder on the Mississippi Remastered are type 87, subtype $00, 32 ROM banks of 8K at $8000, and no $DF00 chunks. The store ships beside the image as EEPRom 8k.bin (8192 bytes) and SRAM 128K.bin (131072 bytes), and the bundled instructions say to select both in VICE's settings before launching the CRT -- exactly the arrangement section 87 describes.

@crystalct -- one thing only you can settle. Under the numbering gpz has confirmed, subtype $00 declares a 32KiB EEPROM, but the release ships an 8KiB one; subtype 1 is the 8K pairing. Was $00 intended?

I ask because current trunk refuses the release's own EEPROM image with MAGICDESKPLUS: Invalid EEPROM image size (must be 32768 bytes), under subtype 0 and subtype 1 alike. That part is a VICE bug rather than anything about your file, reported as 2258 and still open -- but it does mean the emulator cannot currently be used as the reference for how an 8K Magic Desk Plus behaves.

Separately: mdplustest.crt is a far better mapper exerciser than a game, and it is now a loader regression case on this branch. Thank you for it.

@enver-haase

Copy link
Copy Markdown
Contributor Author

Confirmed with the cartridge author's own diagnostics

Following the mapper being green, I ran crystalct's Magic Desk Plus Manager on hardware. It is embedded in the release itself, in banks 7 and 8, so this is the cartridge designer's code exercising the gateware rather than ours.

On an Ultimate II+L in a C64 Ultimate, running a2124a8f, with Murder on the Mississippi Remastered attached:

  • SRAM: DETECTED
  • SRAM diagnostics, integrity self-test: TEST PASSED! SRAM IS INTACT -- all 128K walked through the $DF00 window
  • EEPROM 32KB: DETECTED
  • EEPROM diagnostics: passes, walking well past page $1F into the $70s

Both stores come up NOT INITIALIZED, which is correct: the release carries no $DF00 chunks and this device has no way to attach the EEPRom 8k.bin and SRAM 128K.bin that ship beside it, so the loader erases both areas. Detected but blank is exactly the expected state.

This confirms a2124a8f on hardware. The release declares revision 0, the loader now takes the page mask from the header rather than from the absent EEPROM chunk, and the Manager sees a 32KB part. Before that commit the same file reported EEPROM 8KB -- the page count in the diagnostics run corroborates it, since an 8K part is 32 pages and this one walks past 112.

@crystalct -- two things for you.

First, the substantive one, now with hardware behind it rather than my reading of a header: the release declares revision 0, a 32KiB EEPROM, and ships an 8KiB image file. Your own Manager, on real hardware, reports 32KB. Which is authoritative -- should the header be revision 1, or is the 8K image the odd one out?

Second, a cosmetic bug in the Manager: EEPROM DIAGNOSTICS prints TEST PASSED! SRAM IS INTACT on success. Bank 7 holds only one pass string and it names SRAM, so the EEPROM test borrows it. The test itself is fine -- only the message is wrong.

@crystalct

Copy link
Copy Markdown
  1. I think you are referring to the revisions devised by the Vice Team. I can explain the philosophy behind the hardware design. It supports both the 8KByte and 32KByte EEPROMs, as the pinout is compatible. The only way to determine which one is installed (barring cases where the board designer loads the software themselves and thus knows exactly which type is present) is to run a software test: either by checking for a specific signature byte pair, or by writing and verifying the signature—first at the 32KByte chip's final address, and then, if that fails, at the 8KByte chip's final address. If both attempts fail, the EEPROM is either missing or non-functional; after all, the EEPROM is an optional component. However, in a CRT setup, one can safely assume the 32KB version is always present; unlike the VICE methodology, we are not bound by pre-loading binary files for SRAM and EEPROM to define their actual sizes and presence.

  2. That is actually a bug in my software, so it is irrelevant here.

@mrdudz

mrdudz commented Sep 18, 2026 •

Copy link
Copy Markdown

unlike the VICE methodology, we are not bound by pre-loading binary files for SRAM and EEPROM to define their actual sizes and presence.

you don't have to do that in VICE either, the EEPROM size is defined by either the subtype in the CRT file, or the respective option in the UI (if you attach a binary cartridge image)

(and indeed type 0 was made 32k eprom + sram since that will always work)

@enver-haase

Copy link
Copy Markdown
Contributor Author

Both points are addressed, and the second one turned out to be worth asking.

1. The E2E suite is in. tests/e2e/io/c64/magicdesk_plus_cartridge_test.py builds a synthetic Magic Desk Plus CRT, copies a 6502 routine to $C000 and runs it from RAM, and leaves its answers in screen RAM, following the pattern of the tests you pointed at. Against your list:

  • $DE00: bank selection across bits 0..6 over all 128 banks, and bit 7 switching the ROM off.
  • $DE01 and the $DF00 window: page selection and read-back of the page contents, plus write probes that write through the window and read the bytes back from both SRAM halves.
  • $DE03: EEPROM versus SRAM via bit 5, and the SRAM portion via bit 0. One probe deliberately asks a 32K EEPROM for a page above its size.
  • Window persistence: rom_off_probes() sets $DE00 bit 7 and then reads and writes the window, which has to keep answering.

Registered in both places you asked for: run-tests:531 as the magicdesk-plus-cartridge suite, gated on machine.MAGIC_DESK_PLUS_MAPPER, and in the matrix at tests/README.md:299. There is also a VHDL simulation testbench, fpga/cart_slot/vhdl_sim/tb_magic_desk_plus.vhd.

2. The generic: you are right, and the picture is mixed. The mapper puts its seven bank bits at bank_bits(20 downto 14):

bank_bits(21 downto 14) <= '0' & io_wdata(6 downto 0);

and all_carts_v5.vhd:730 forwards only bank_bits(g_max_cart_bits-1 downto 13) into rom_addr. So the top bank bit needs g_max_cart_bits >= 21; at the default of 20 it is dropped, io_wdata(6) has no effect, and the upper 64 banks alias onto the lower ones. Exactly the check you asked for.

Where it stands per file:

u2p_nios2_solo.vhd:530      g_max_cart_bits => 22   -- "4 MB!"
u2p_riscv_lattice.vhd:603   g_max_cart_bits => 22   -- "4 MB!"
tb_magic_desk_plus.vhd:67   g_max_cart_bits => 22

against defaults of 20 in all_carts_v5.vhd:13, slot_server_v4.vhd:21 and ultimate_logic_32.vhd:40. Several top levels instantiate the logic without setting the generic and therefore inherit 20: u2p_riscv.vhd, ultimate_mb_700a.vhd, ultimate_riscv_700a.vhd and u2_loader_riscv.vhd, besides the test harnesses. I have not traced which target/fpga/* build directory maps onto which of those files, so I am reporting the files rather than claiming a particular product is affected.

The U64 family cannot be checked from here at all: there is no U64 top level in this repository, and the bitstreams ship prebuilt in external/ (u64.sof, u64e2_50t.bit, u64e2_100t.bit) from another tree. @GideonZ, what is g_max_cart_bits on that side?

The remaining decision is whether to raise the three defaults from 20 to 22 or to set the generic explicitly on the remaining top levels. I would rather not pick one without knowing what the memory map on the smaller targets can afford.

Ready for another look at the test side in any case.

enver-haase and others added 8 commits September 25, 2026 14:06
Magic Desk Plus is a Magic Desk with three things bolted on: one more bank bit,
so DE00 selects 128 banks of 8K instead of 64, a page register at DE01, and a
control register at DE03 that switches a 256 byte window at DF00 between 128K
of battery-backed SRAM and an 8K or 32K EEPROM. The window is readable and
writable, and stays served when the ROM is switched off -- the file system the
format ships with depends on that.

The SRAM and the EEPROM go in the memory the REU and GeoRAM already share
rather than in an area of their own. That region exists on every target, which
an area of its own would not: above the 64K of cartridge RAM there is a free
megabyte on U64, U64-II and U2+L, but on the U2 the cartridge ROM starts right
there. The cost is that this cart and the REU cannot both be on, which is what
GeoRAM already does and what the prohibit mechanism already handles.

tb_magic_desk_plus drives the registers the way the machine does and checks the
address the logic produces. Against this change all 20 checks pass; against the
unchanged file 17 fail. It also pins down something the file does not say
anywhere: cart_variant is sampled only while the cartridge is in reset, so the
EEPROM size cannot be changed without one.

For GideonZ#727.
Magic Desk and Magic Desk Plus are the same CRT hardware type. The upstream
implementation confirms it: the VICE patch that comes with the format extends
magicdesk.c rather than adding a cartridge, and its attach path derives nothing
from the file but a bank mask, taken from the highest bank present. What turns
the SRAM and the EEPROM on there is the user supplying an image for them.

So nothing in the header can tell the two apart, and this uses the same thing
VICE does: a cart that brought its non-volatile memory with it is a Plus. It
travels in the CRT as chunks at DF00, the address of the window they are
reached through, and the bank field says which piece each chunk is. It has to:
the size field of a CHIP header is 16 bits, so the 128K of SRAM cannot be one
chunk and is carried in four quarters of 32K. Bank 0 is the EEPROM, banks 1 to
4 are the SRAM in address order. A chunk that is none of those shapes is
refused rather than loaded.

The EEPROM size chooses the page mask, again as in VICE: 8K masks the page
register to 0x1F and 32K to 0x7F, which is what its io2 handlers do, and it
accepts an EEPROM image only at those two sizes. A cart carrying only SRAM gets
the 8K mask, the size VICE itself creates when it has to make an EEPROM image
from nothing.

The cart prohibits the whole of IO rather than only DEXX. Its registers are at
DE00..DE03 and its window is the entire DF00 page, so the UCI at DF1C, the
sampler, an ACIA at either address and the REU whose memory this borrows all
have to give way.

A Magic Desk without any of this keeps the mapping it has always had.

For GideonZ#727.
A CHIP chunk of $8000 bytes is how the loader recognises a C128 image, and it
asked that question of every chunk before it looked at what the chunk was. A
Magic Desk Plus SRAM chunk is exactly $8000 bytes, so a file that carries its
store before its ROM turned every ROM bank into a 32K one: 127 of 128 banks
then sat at the wrong offset, and auto_mirror() left the image unmirrored as
well. The cartridge reads whatever is at bank * 16K, so the machine starts in
the wrong bank with nothing to say why.

The test moves the question behind the two store branches, which return before
it, so it now sees ROM chunks only.

The host test grew a Magic Desk Plus section: the type and variant the loader
selects for an 8K EEPROM, a 32K one and SRAM alone, the I/O it prohibits, where
each store chunk lands, that an unfilled part of the store reads as erased
rather than as what the last cartridge left in the REU, the bank offsets in
both chunk orders, and six store layouts the loader has to refuse.

    with the old bound   c64_crt_test: FAIL (581 checks, 1 failed)
    with this commit     c64_crt_test: OK   (581 checks, 0 failed)

host_reu_memory was static in the host header, so the loader and the test each
had one of their own and the test could never see what the loader wrote. It is
declared here and defined by the test.
The cartridge logic has run in GHDL simulation only. This adds the E2E suite
that measures it on a machine, built like the other cartridge suites: no ROM
image is shipped, the CRT is generated in code.

It is a 128-bank type 19 image whose store travels in CHIP chunks at $DF00, a
32K EEPROM and 128K of SRAM. Every page of the store carries its own page
number in byte 0 and a tag for its area in byte 1, so two bytes read through
the window say which page of which area it reached. Bank 0 autostarts, copies a
routine to $C000 and runs it from RAM, since the first $DE00 write replaces the
ROM it would otherwise be executing. The routine selects all 128 banks and
copies each marker to screen RAM, switches the ROM off with bit 7 and reads the
RAM underneath, points the window at six places including page $85 of a 32K
EEPROM, which is masked to $05, writes a byte into the window and reads the
same page of the other SRAM half to show the write did not land in both, and
finally reads and writes the window with the ROM switched off, which is what
the store's file system relies on.

The mapper is cartridge logic in the FPGA image rather than firmware, and no
shipped image has it: the Ultimate 64 and C64 Ultimate images are prebuilt in
external/ and come from another tree, and an Ultimate II+ has it once fpga/ is
rebuilt from this branch. All three kinds are therefore listed under the new
magic-desk-plus-mapper entry, and the suite reports SKIP until one of them is
measured with --assume-fix.

The routine is assembled by hand, so it was dry-run against a model of the
mapper read from the VHDL before any of this was proposed as a test: no
mismatch against this branch, eleven against the logic before it, where banks
64 to 127 alias onto 0 to 63 and the window reads $FF. The one check green in
both is the control: a plain Magic Desk also switches its ROM off with bit 7.
VICE registers Magic Desk Plus as cartridge type 87:

    #define CARTRIDGE_MAGIC_DESK_PLUS      87 /* magicdeskplus.c */
    #define CARTRIDGE_LAST                 87

and every released image carries it. Murder on the Mississippi Remastered,
which is why GideonZ#727 was opened, is a type 87 file of 32 ROM banks named "Magic
Desk Plus" in its header. This tree had 87 for TwoMegabyter, so that image
loaded as a TwoMegabyter and the machine came up to a blue screen, which is
what a user reported on the release page in July.

TwoMegabyter has no assigned id at all. make_crt_twomeg.py says so itself and
asks to be updated once one exists, so it moves to the next free number here in
anticipation, and Magic Desk Plus takes the one that is actually its own.

That also removes the guess the loader had to make. It recognised Magic Desk
Plus by finding store chunks on a type 19 image, because nothing in the header
told it apart from a plain Magic Desk; the header tells it now. The guess would
not have worked anyway: VICE keeps the SRAM and EEPROM in files beside the
image rather than in it, and the released game carries neither. An image that
brings no store now finds one erased, because the store lives in the memory the
REU uses and would otherwise hold whatever the cartridge before it left there.

The store chunks stay: they are how Save Cartridge writes the store back, so a
saved game survives the next load.

Measured on the released files, all three languages, through the loader itself:

    rc=0  type=$12 (Magic Desk Plus)  prohibit=$09F  variant 0 (8K page mask)
    EEPROM area erased: yes    SRAM area erased: yes
    bank 0 at $8000: 09 80 39 80 C3 C2 CD 38 30   ("CBM80" autostart)

The 8K mask matches the EEPRom 8k.bin the release ships beside the image.
… tested

The loader tests build their own headers, so they confirm the loader agrees
with this file's idea of a CRT rather than with a real one. crystalct offered
mdplustest.crt in GideonZ#844 as the image to test the mapper with: 262720 bytes of
32 ROM banks at $8000 and nothing else, no $DF00 chunk and no store. That is
the released shape, since VICE keeps the SRAM and the EEPROM in files beside
the image.

So take its header byte for byte and load that shape at both 1 MB and 4 MB,
the second because 1 MB is what a cartridge region is unless a target asks for
more and no Magic Desk Plus case covered it. The bank payloads stay this
file's own: what is regressed is the shape and the header, not the manager's
6502.

What it catches that the built headers do not: a storeless cart taking the 32K
page mask instead of the 8K one. VICE masks to $1F when it has to make an
EEPROM image from nothing, and the manager in this very image displays that
choice as "EEPROM 8KB: DETECTED".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The file says the window stays served whether or not the ROM is off, and
the cartridge did not do it. On hardware, writing $DE00 bit 7 stopped
$DF00 answering: a read came back $FF instead of the stored byte, and a
write did not stick.

serve_enable is built from cart_en, so clearing cart_en for the ROM
disable took IO2 down with it. The ROM has to go away by a different
route: slot_slave decodes $8000-$BFFF on serve_rom alone, without
consulting ROMLn, so dropping serve_rom hands that range back to the RAM
under the cartridge, which is what the machine should see there. cart_en
then stays high and the window keeps answering.

Measured on an Ultimate II+L in a C64 Ultimate, from the CI build of this
branch:

  [11] the window still reads with the ROM off    FAIL  probe 2 read $FF, want $05
  [12] the window still takes a write with the ROM off  FAIL  probe 3 read $FF, want $5A

Three attempts, all the same. The other eleven checks passed, including
the 128 bank selections, the $85 to $05 page mask, both SRAM halves, and
$9FF0 reading the RAM under the cartridge with the ROM off -- that last
one is why serve_rom has to drop rather than cart_en staying the gate.

tb_magic_desk_plus did not catch this and could not: it drives the
cartridge logic and checks the addresses it produces, while serve_enable
gates a layer above it.

One visible consequence: cart_active follows cart_en and drives
CART_LEDn, so the cartridge LED now stays lit while a program has the ROM
switched off. The cartridge is still serving its window, so that reads as
the more truthful of the two.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Which parts a Magic Desk Plus has is declared in the CRT header's hardware
revision byte. This read it out of the EEPROM chunk instead, which gets a
released image wrong: a release carries no store at all and still has both
parts, so the absent chunk was read as "8K" and the page register was
masked to $1F where the hardware masks it to $7F.

All three language builds of Murder on the Mississippi Remastered are
revision 0, 32 ROM banks and nothing else, with the store in EEPRom 8k.bin
and SRAM 128K.bin beside the image. Under the old rule every one of them
came out with the wrong mask.

The numbering was worth waiting for. VICE's cartconv, its emulator and its
manual gave byte $1A three different meanings, so any choice was a guess;
reported as vice-emu bug 2257 and closed fixed in r46239, with the
emulator authoritative:

  0  SRAM + 32K EEPROM      3  8K EEPROM
  1  SRAM + 8K EEPROM       4  SRAM only
  2  32K EEPROM

A revision the format does not define is refused in check_header, where a
result code can still be returned. One that fits no EEPROM leaves the mask
with nothing to choose and takes $1F, which it never uses.

The loader test now states the contract the other way round: the rows
declare a revision and expect a mask, and one row carries a 32K chunk
under revision 1 so that a return to reading the chunk cannot pass. The
mdplustest.crt regression case expects $7F, having expected $1F when it
was written a few hours ago -- it is revision 0, and the file carries no
EEPROM chunk to infer from at all.

621 checks, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	software/io/c64/tests/c64_crt_test.cc
@enver-haase

Copy link
Copy Markdown
Contributor Author

@mrdudz, for the "existing hacks" TODO in your CRT V3 draft, here is what the 1541 Ultimate does with GMod2 (type 60), from software/io/c64/c64_crt.cc on master. (And a typo in the draft's header: "WOK IN PROGRESS".)

  • The EEPROM travels as one CHIP packet: chip type 0 (ROM), bank 0, load address $DE00, size $0800 (2 KiB).
  • The loader recognises it by the load address alone: any CHIP packet loading at $DE00 is read as EEPROM data, whatever its chip type and whatever the cartridge type. One larger than $0800 is refused, and so is a second one.
  • If a GMod2 CRT has no such packet and the core has the EEPROM, the firmware adds one filled with $FF when the cartridge is loaded.
  • Nothing is written back on its own: "Save Cartridge" in the menu writes the whole CRT, with the EEPROM contents read back from the hardware if they changed.

So the chip type field says ROM, not the 3 (EEPROM) the current spec lists as informal.

@chrisgleissner chrisgleissner added the cart Cartridge emulation, external cartridge compatibility, REU, and GeoRAM. label Oct 3, 2026
@enver-haase enver-haase changed the title Magic Desk Plus cartridge support (#727) — simulated only, never run Magic Desk Plus cartridge support (#727) Oct 6, 2026
@chrisgleissner

Copy link
Copy Markdown
Collaborator

I merged master into this branch to resolve a conflict in tests/lib/machine.py. Master removed the C64GS_BANK_FROM_ADDRESS and COMAL80_CARTRIDGE_OFF_BIT entries, because the C64GS and COMAL 80 checks now run on every machine. I kept only MAGIC_DESK_PLUS_MAPPER, and nothing else in the tree uses the two removed names.

The device-free suites pass on the merged tree (lint, registry, runner policy, observability). This is a merge commit only, so no history was rewritten. Revert it if you prefer to rebase yourself.

@enver-haase

Copy link
Copy Markdown
Contributor Author

@chrisgleissner, could you take another look when you have time? Your review of 14 September still shows as "changes requested", and I think both points are covered, as written on 19 September (#844 (comment)):

  1. E2E suite: tests/e2e/io/c64/magicdesk_plus_cartridge_test.py, registered in run-tests (magicdesk-plus-cartridge) and in tests/README.md. It covers your four points ($DE00 over all 128 banks and bit 7, $DE01 with the $DF00 window, $DE03 EEPROM/SRAM select, the window with the ROM off). It ran green on an Ultimate II+L, 13 checks, after it had found a real bug: the window went dark with the ROM off.
  2. g_max_cart_bits: checked. The U2+ and U2+L top levels set 22; several others inherit the default of 20, and the U64 side is not in this repository. Which way to fix that is a question for @GideonZ, listed in the description.

What keeps the PR in draft are those decisions and the store format, which waits for VICE's CRT V3. Neither is on the test side. CI is green on 727ed7f as of this morning.

@chrisgleissner chrisgleissner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two items from my 14 September review are covered. The E2E suite is registered in run-tests and tests/README.md and covers the four points I listed. g_max_cart_bits is 22 on the U2+ and U2+L top levels, so all 128 banks are addressable there. Thanks for the work on both.

I found two problems in the change itself and one open question on the CRT type numbers.

1. The EEPROM address includes DE03 bit 0, which only selects the SRAM half

In all_carts_v5.vhd the MDP address is built from mdp_sram & mdp_half & mdp_page_m, and mdp_half is used even when mdp_sram = '0'. With DE03 bit 5 clear and bit 0 set, the window therefore reads the second 64K of the EEPROM area. The loader never fills that range, so it returns whatever the REU left there. VICE's magicdeskplus.c uses bit 0 only on the SRAM path and ignores it for the EEPROM.

A program that moves from SRAM half 1 ($21) to the EEPROM by clearing only bit 5 ($01) reads and writes the wrong memory. Writes land in a range that Save Cartridge never saves. Neither tb_magic_desk_plus.vhd nor the E2E suite reads the EEPROM with bit 0 set, so both stay green.

Suggested change: use mdp_half and mdp_sram in the address, and add a probe to both tests that selects the EEPROM with DE03 = $01 and expects the same page as with $00.

2. The firmware does not check that the FPGA image has the mapper

configure_cart selects CART_TYPE_MDPLUS on every machine. On an image without this mapper, the cartridge type has no case in all_carts_v5.vhd. The defaults then leave GAME and EXROM high and serve_rom low. The CRT loads without an error and the machine boots to BASIC with no cartridge ROM visible. This will happen to anyone who updates the firmware before the FPGA image that carries this change is available, which is the situation on the U64 family and the C64U as the description says.

GMod2 refuses to load without CAPAB_EEPROM. This cartridge needs an equivalent: a capability bit, or another signal Gideon prefers, so that check_header returns "not implemented" on an image that cannot serve it. This is a decision for @GideonZ, but it should be listed under Open in the description.

3. Renumbering TwoMegabyter from 87 to 88

make_crt_twomeg.py and the loader table move TwoMegabyter to 88. Any TwoMegabyter CRT already generated with type 87 now loads as Magic Desk Plus and is mapped incorrectly, without an error. 88 is also not assigned by VICE, so it may collide with a later assignment. Gideon assigned 87 to TwoMegabyter earlier in this thread, and this PR changes that, so the description should list it as an open decision.

I am leaving this at changes requested until 1 is fixed. 2 and 3 can wait for Gideon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3.16 Targeted for release 3.16. cart Cartridge emulation, external cartridge compatibility, REU, and GeoRAM. enhancement New functionality or an improvement to existing behavior, including formats and developer tooling.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants