Skip to content

firmware: add the D64/D71/D81 engine that GEOS, the block device and VDrive will share - #88

Open
wesbiggs wants to merge 7 commits into
SensoriumEmbedded:mainfrom
wesbiggs:shared-disk-engine
Open

wesbiggs wants to merge 7 commits into
SensoriumEmbedded:mainfrom
wesbiggs:shared-disk-engine

Conversation

@wesbiggs

@wesbiggs wesbiggs commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

This is part 1 of a series of PRs to streamline and consolidate disk image (.d64/.d71/.d81) access, which will then be used to power the VDrive Special IO. This is a big hunk of code by itself, which is why I'm separating it out like this.

Rationale

The GEOS installer and the TR block device each carry their own image geometry, BAM and directory code, and VDrive brings a third, more complete engine (40-track D64s, error bytes, BAM and directory growth for all three kinds, tested against VICE's c1541). This brings VDrive's engine in under Source/Teensy/DiskImage/ so the other two can move onto it.

State of affairs after this PR

Nothing uses it yet. The copy differs from teensyrom-vdrive's d64/ (commit b74ce37) only in what the firmware needs: SPDX lines, the placement macros moving into d64.h, and its three constant tables marked D64_FLASH so they land in PROGMEM rather than DTCM. DiskImage.h includes the code once, for the single translation unit the firmware and the host harnesses build.

Follow-up PRs move the block device, then the GEOS installer and boot-file reader, then the TR menu's D64/D71/D81 loading and directory listing onto it.

Memory

  • Every engine function is VD_COLD, which DiskImage.h defines as FLASHMEM on the firmware. None of it is reachable from the PHI2 ISR or an IO handler's cycle path.
  • The constant tables are PROGMEM.
  • The engine never calls malloc and adds no globals: all state lives in structs the caller owns, so it holds nothing in RAM1 or RAM2 between uses.
  • Nothing in either firmware build includes DiskImage.h yet, so this PR doesn't change the TR or TR+ images. The RAM cost will show up in the follow-up PRs that call it, and those will report it.

Docs

  • New docs/Architecture/Disk-Images.md: what the engine is, where it came from, how a caller uses it, the size-to-format table, and its tests.
  • Linked from docs/Architecture/Overview.md.

Testing

  • New host test Source/Teensy/tests/disk-image.test.js, which builds the engine with the firmware's warning flags as both C and C++. On a blank image of each kind it saves a file and reads it back, and it checks the size-to-format table.
  • npm test passes, and CI is green on both firmware builds (tr, tr-plus) and the tools job.
  • Host build only, not hardware tested. Nothing on the board runs this code until the follow-up PRs.

Compatibility

No change to the remote protocol (docs/ControlComms.md), the cartridge register map, the EEPROM settings or any timing.

License

The engine is my own work from teensyrom-vdrive, which is MIT licensed. The files here carry SPDX-License-Identifier: MIT.

🤖 Generated with Claude Code

…VDrive will share

The GEOS installer and the TR block device each carry their own image
geometry, BAM and directory code, and VDrive brings a third, more
complete engine (40-track D64s, error bytes, BAM and directory growth
for all three kinds, tested against c1541). Bring VDrive's engine in
under Source/Teensy/DiskImage so the other two can move onto it.

Nothing uses it yet. The copy differs from teensyrom-vdrive's d64/ only
in what the firmware needs: SPDX lines, the placement macros moving into
d64.h, and its three constant tables marked D64_FLASH so they land in
PROGMEM rather than DTCM. DiskImage.h includes the code once for the
single translation unit the firmware and the host harnesses build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5t9wWXadFkQgKaCnjreF7

@kfox kfox left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I reviewed the engine on the host. I built small C programs against DiskImage.h with fake reader/writer callbacks; I did not test it on hardware. Three defects and one design note are inline. None of them can happen on a board yet, since nothing calls the engine, but VDrive's write path will reach all of them.

Comment thread Source/Teensy/DiskImage/d64.c Resolved
Comment thread Source/Teensy/DiskImage/d64.c Resolved
Comment thread Source/Teensy/DiskImage/d64.h Resolved
Comment thread Source/Teensy/DiskImage/d64.c Resolved
wesbiggs and others added 6 commits November 6, 2026 15:47
…a 1541 does

The allocator searched outward from the file's last sector and reported the
disk full when it reached the end of the allocation order, so a file
appended to after a scratch freed earlier tracks failed with blocks free.
A hint on the directory track, D71 track 53 or D64 tracks 36-40 failed on
its first new block. The search now restarts once from the start of the
order before it reports FULL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… write does

When the last data sector or the directory entry could not be written,
d64_close_write returned the error and left the entry unclosed with its
blocks allocated. A new file or a copy left a splat file and leaked blocks,
contrary to d64_copy's "nothing is left behind"; an append left the original
file unclosed, so the next validate deleted all of it. The close now aborts
the write as d64_write does on an error, which puts an appended file back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… does

d64_create with replace scratched the old file before writing the new one,
so a replace that ran out of room lost the old file. The old entry is now
kept in the write handle and scratched once the new file has closed; a
write that fails leaves it as it was. Both files need room at once, so a
replace can now fail on a nearly full disk where scratching first would
have fit, which is also what a 1541 does. A locked file is refused before
anything is written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ritten

d64_create now checks the lock itself instead of relying on the scratch
it no longer does first; without the check the new file is written and
the close fails with both files left on the disk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… image

It returned 0 both when the buffer was too small and when the header,
directory or BAM could not be read, so a caller could only report an
unreadable image as a buffer problem. It now returns the length, the new
D64_E_BUF for a small buffer, or the read's own error (D64_E_IO,
D64_E_LOOP for a looping directory chain).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It skips d64_format's clearing pass, so on a used image the old data stays
in every sector it does not write. The comment said what it writes but not
that the caller must guarantee the rest is already zero.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@wesbiggs

wesbiggs commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review, and for building against the engine on the host. The repros made each fix easy to pin down.

All four points are addressed, each with a host test in Source/Teensy/tests/disk_image_harness.c. I checked that every new case fails on the engine as it was:

  • Disk full with blocks free: the allocator searches the rest of the disk before reporting FULL (1153fec).
  • Failed close: the file being written is removed, and an append puts the original back (512683e).
  • d64_build_dir: separate return codes, so a small buffer and an unreadable image no longer look the same (050deb8).
  • @:: the old file is kept until the new one closes. A replace now needs room for both files, which d64.h documents (029f455, plus a locked-file test in 7a1d9e7).

One more change you didn't raise: the d64_format_blank comment now says to call it only on an image known to be all zeros, since it skips d64_format's clearing pass (6ebd42c).

npm test passes. The engine still isn't compiled into the firmware in this PR, so these changes don't affect its build. The branch that does compile it in builds for both TR and TR+ with these fixes, with RAM1 unchanged. Host build only, not hardware tested.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants