From 74bda54e8809b69afe5d20944c29e3d70223013d Mon Sep 17 00:00:00 2001 From: David Given Date: Sat, 5 Sep 2026 22:23:45 +0200 Subject: [PATCH 1/2] Add support for the ZX Spectrum TR-DOS format (thanks to Sonic-Amiga@github for contributing this). --- java/com/cowlark/fluxengine/arch/trdos.textpb | 126 ++++++++++++++++++ .../fluxengine/config/ConfigBuilder.java | 62 +++------ .../fluxengine/config/ImageFormats.java | 53 ++++++++ 3 files changed, 199 insertions(+), 42 deletions(-) create mode 100644 java/com/cowlark/fluxengine/arch/trdos.textpb create mode 100644 java/com/cowlark/fluxengine/config/ImageFormats.java diff --git a/java/com/cowlark/fluxengine/arch/trdos.textpb b/java/com/cowlark/fluxengine/arch/trdos.textpb new file mode 100644 index 00000000..068bd358 --- /dev/null +++ b/java/com/cowlark/fluxengine/arch/trdos.textpb @@ -0,0 +1,126 @@ +shortname: 'TR-DOS' +comment: 'TR-DOS for ZX-Spectrum BetaDisc interface' +read_support_status: UNICORN +write_support_status: UNICORN + +documentation: +<<< +The BetaDisc interface for the ZX-Spectrum by Technology research came with a simple +operating system in ROM named TR-DOS. The controller is a standard 1793 chip, +widespread at the time. 40 or 80 tracks, one or two sides, 16 sectors per track, +sector size is 256 bytes. Side byte in sector ID record is not used and always +set to 0. + +FluxEngine can read and write these. +>>> + +image_reader { + filename: "trdos.trd" + type: IMAGETYPE_IMG +} + +image_writer { + filename: "trdos.trd" + type: IMAGETYPE_IMG +} + +decoder { + ibm { + trackdata { + ignore_side_byte: true + } + } +} + +encoder { + ibm { + trackdata { + emit_iam: false + gap0: 10 + gap2: 22 + gap3: 60 + } + } +} + +option_group { + comment: "$formats" + + option { + name: "640dsdd" + comment: '640kB 80-track double-sided' + set_by_default: true + + config { + layout { + format_type: FORMATTYPE_80TRACK + tracks: 80 + sides: 2 + layoutdata { + sector_size: 256 + physical { + start_sector: 1 + count: 16 + } + } + } + } + } + option { + name: "320ssdd" + comment: '320kB 80-track single-sided' + + config { + layout { + format_type: FORMATTYPE_80TRACK + tracks: 80 + sides: 1 + layoutdata { + sector_size: 256 + physical { + start_sector: 1 + count: 16 + } + } + } + } + } + option { + name: "320dsdd" + comment: '320kB 40-track double-sided' + + config { + layout { + format_type: FORMATTYPE_40TRACK + tracks: 40 + sides: 2 + layoutdata { + sector_size: 256 + physical { + start_sector: 1 + count: 16 + } + } + } + } + } + option { + name: "160SSDD" + comment: '160kB 40-track single-sided' + + config { + layout { + format_type: FORMATTYPE_40TRACK + tracks: 40 + sides: 1 + layoutdata { + sector_size: 256 + physical { + start_sector: 1 + count: 16 + } + } + } + } + } +} diff --git a/java/com/cowlark/fluxengine/config/ConfigBuilder.java b/java/com/cowlark/fluxengine/config/ConfigBuilder.java index e7f3cfa6..8901894a 100644 --- a/java/com/cowlark/fluxengine/config/ConfigBuilder.java +++ b/java/com/cowlark/fluxengine/config/ConfigBuilder.java @@ -13,6 +13,7 @@ import static com.cowlark.fluxengine.config.FluxSourceSinkType.FLUXTYPE_SCP; import static com.cowlark.fluxengine.config.FluxSourceSinkType.FLUXTYPE_TEST_PATTERN; import static com.cowlark.fluxengine.config.FluxSourceSinkType.FLUXTYPE_VCD; +import static com.cowlark.fluxengine.config.ImageFormats.Mode.MODE_RO; import static com.cowlark.fluxengine.config.ImageReaderWriterType.IMAGETYPE_D64; import static com.cowlark.fluxengine.config.ImageReaderWriterType.IMAGETYPE_D88; import static com.cowlark.fluxengine.config.ImageReaderWriterType.IMAGETYPE_DIM; @@ -25,6 +26,7 @@ import static com.cowlark.fluxengine.config.ImageReaderWriterType.IMAGETYPE_NSI; import static com.cowlark.fluxengine.config.ImageReaderWriterType.IMAGETYPE_TD0; +import com.cowlark.fluxengine.config.ImageFormats.ImageFormat; import com.cowlark.fluxengine.core.Logger; import com.cowlark.fluxengine.core.flags.FlagGroup; import com.cowlark.fluxengine.core.flags.Flags; @@ -54,42 +56,6 @@ public ConfigBuilder() { } - private static ImageReaderWriterType imageType(String filename) - { - if (filename.endsWith(".adf") || filename.endsWith(".d81") || filename.endsWith(".dsk") || - filename.endsWith(".img") || filename.endsWith(".st") || - filename.endsWith(".vgi") || filename.endsWith(".xdf")) - return IMAGETYPE_IMG; - else if (filename.endsWith(".d64")) - return IMAGETYPE_D64; - else if (filename.endsWith(".d88")) - return IMAGETYPE_D88; - else if (filename.endsWith(".dim")) - return IMAGETYPE_DIM; - else if (filename.endsWith(".diskcopy")) - return IMAGETYPE_DISKCOPY; - else if (filename.endsWith(".fdi")) - return IMAGETYPE_FDI; - else if (filename.endsWith(".imd")) - return IMAGETYPE_IMD; - else if (filename.endsWith(".jv3")) - return IMAGETYPE_JV3; - else if (filename.endsWith(".nfd")) - return IMAGETYPE_NFD; - else if (filename.endsWith(".nsi")) - return IMAGETYPE_NSI; - else if (filename.endsWith(".td0")) - return IMAGETYPE_TD0; - else - return null; - } - - private static boolean isReadOnlyImage(String filename) - { - return filename.endsWith(".dim") || filename.endsWith(".fdi") || - filename.endsWith(".jv3") || filename.endsWith(".nfd") || filename.endsWith(".td0"); - } - /* Quotes a string if it contains spaces or quote characters, ported from * lib/core/utils.cc quote(). */ private static String quote(String s) @@ -258,21 +224,33 @@ private void setFluxSink(FluxSinkProto.Builder fluxSink, String filename) throw new ConfigException("unrecognised flux filename '" + filename + "'"); } + private ImageFormat findImageFormat(String filename) + { + for (ImageFormat format : ImageFormats.imageFormats) + { + if (filename.endsWith(format.extension())) + return format; + } + return null; + } + public ConfigBuilder withImageWriter(String filename) { - ImageReaderWriterType type = imageType(filename); - if (type == null || isReadOnlyImage(filename)) + ImageFormat format = findImageFormat(filename); + if (format == null) throw new ConfigException("unrecognised image filename '" + filename + "'"); - proto.getImageWriterBuilder().setType(type).setFilename(filename); + if (format.mode() == MODE_RO) + throw new ConfigException("image filename '" + filename + "' can only be read"); + proto.getImageWriterBuilder().setType(format.type()).setFilename(filename); return this; } public ConfigBuilder withImageReader(String filename) { - ImageReaderWriterType type = imageType(filename); - if (type == null) + ImageFormat format = findImageFormat(filename); + if (format == null) throw new ConfigException("unrecognised image filename '" + filename + "'"); - proto.getImageReaderBuilder().setType(type).setFilename(filename); + proto.getImageReaderBuilder().setType(format.type()).setFilename(filename); return this; } diff --git a/java/com/cowlark/fluxengine/config/ImageFormats.java b/java/com/cowlark/fluxengine/config/ImageFormats.java new file mode 100644 index 00000000..d8f449e4 --- /dev/null +++ b/java/com/cowlark/fluxengine/config/ImageFormats.java @@ -0,0 +1,53 @@ +package com.cowlark.fluxengine.config; + +import static com.cowlark.fluxengine.config.ImageFormats.Mode.MODE_RO; +import static com.cowlark.fluxengine.config.ImageFormats.Mode.MODE_RW; +import static com.cowlark.fluxengine.config.ImageReaderWriterType.IMAGETYPE_D64; +import static com.cowlark.fluxengine.config.ImageReaderWriterType.IMAGETYPE_D88; +import static com.cowlark.fluxengine.config.ImageReaderWriterType.IMAGETYPE_DIM; +import static com.cowlark.fluxengine.config.ImageReaderWriterType.IMAGETYPE_DISKCOPY; +import static com.cowlark.fluxengine.config.ImageReaderWriterType.IMAGETYPE_FDI; +import static com.cowlark.fluxengine.config.ImageReaderWriterType.IMAGETYPE_IMD; +import static com.cowlark.fluxengine.config.ImageReaderWriterType.IMAGETYPE_IMG; +import static com.cowlark.fluxengine.config.ImageReaderWriterType.IMAGETYPE_JV3; +import static com.cowlark.fluxengine.config.ImageReaderWriterType.IMAGETYPE_NFD; +import static com.cowlark.fluxengine.config.ImageReaderWriterType.IMAGETYPE_NSI; +import static com.cowlark.fluxengine.config.ImageReaderWriterType.IMAGETYPE_TD0; + +import com.google.common.collect.ImmutableList; + +public class ImageFormats +{ + public enum Mode + { + MODE_RO, MODE_RW; + } + + public record ImageFormat(String extension, ImageReaderWriterType type, Mode mode) + { + } + + public static ImmutableList imageFormats = ImmutableList.of( + new ImageFormat(".adf", IMAGETYPE_IMG, MODE_RW), + new ImageFormat(".d64", IMAGETYPE_D64, MODE_RW), + new ImageFormat(".d81", IMAGETYPE_IMG, MODE_RW), + new ImageFormat(".d88", IMAGETYPE_D88, MODE_RW), + new ImageFormat(".dim", IMAGETYPE_DIM, MODE_RO), + new ImageFormat(".diskcopy", IMAGETYPE_DISKCOPY, MODE_RW), + new ImageFormat(".dsk", IMAGETYPE_IMG, MODE_RW), + new ImageFormat(".fdi", IMAGETYPE_FDI, MODE_RO), + new ImageFormat(".imd", IMAGETYPE_IMD, MODE_RW), + new ImageFormat(".img", IMAGETYPE_IMG, MODE_RW), + new ImageFormat(".jv3", IMAGETYPE_JV3, MODE_RO), + new ImageFormat(".nfd", IMAGETYPE_NFD, MODE_RO), + new ImageFormat(".nsi", IMAGETYPE_NSI, MODE_RW), + new ImageFormat(".st", IMAGETYPE_IMG, MODE_RW), + new ImageFormat(".td0", IMAGETYPE_TD0, MODE_RO), + new ImageFormat(".trd", IMAGETYPE_IMG, MODE_RW), + new ImageFormat(".vgi", IMAGETYPE_IMG, MODE_RW), + new ImageFormat(".xdf", IMAGETYPE_IMG, MODE_RW)); + + private ImageFormats() + { + } +} From fc2f7ab890d5022704a5a3afb595a9440d883f17 Mon Sep 17 00:00:00 2001 From: David Given Date: Sat, 5 Sep 2026 22:36:40 +0200 Subject: [PATCH 2/2] The tests now work; add proper e2e tests. --- README.md | 1 + doc/disk-trdos.md | 37 +++++++++++++++++++ java/com/cowlark/fluxengine/arch/trdos.textpb | 2 +- .../cowlark/fluxengine/data/FormatsTest.java | 2 +- scripts/corpus.bzl | 4 ++ 5 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 doc/disk-trdos.md diff --git a/README.md b/README.md index fa0e9c7d..eff5e592 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ choices because they can store multiple types of file system. | [`ti99`](doc/disk-ti99.md) | TI-99: 90kB 35-track SSSD | 🦖 | | | | [`tids990`](doc/disk-tids990.md) | Texas Instruments DS990: 1126kB 8" DSSD | 🦖 | 🦖 | | | [`tiki`](doc/disk-tiki.md) | Tiki 100: CP/M | | | CPMFS | +| [`trdos`](doc/disk-trdos.md) | TR-DOS: TR-DOS for ZX-Spectrum BetaDisc interface | 🦄 | 🦄 | | | [`victor9k`](doc/disk-victor9k.md) | Victor 9000 / Sirius One: 1224kB 5.25" DSDD GCR | 🦖 | 🦖 | | | [`zilogmcz`](doc/disk-zilogmcz.md) | Zilog MCZ: 320kB 8" 77-track SSSD hard-sectored | 🦖 | | ZDOS | {: .datatable } diff --git a/doc/disk-trdos.md b/doc/disk-trdos.md new file mode 100644 index 00000000..43167e5c --- /dev/null +++ b/doc/disk-trdos.md @@ -0,0 +1,37 @@ +TR-DOS +==== +## TR-DOS for ZX-Spectrum BetaDisc interface + + +The BetaDisc interface for the ZX-Spectrum by Technology research came with a simple +operating system in ROM named TR-DOS. The controller is a standard 1793 chip, +widespread at the time. 40 or 80 tracks, one or two sides, 16 sectors per track, +sector size is 256 bytes. Side byte in sector ID record is not used and always +set to 0. + +FluxEngine can read and write these. + +## Options + + - Format variants: + - `640dsdd`: 640kB 80-track double-sided + - `320ssdd`: 320kB 80-track single-sided + - `320dsdd`: 320kB 40-track double-sided + - `160sssd`: 160kB 40-track single-sided + +## Examples + +To read: + + - `fluxengine read -c trdos --640dsdd -s drive:0 -o trdos.trd` + - `fluxengine read -c trdos --320ssdd -s drive:0 -o trdos.trd` + - `fluxengine read -c trdos --320dsdd -s drive:0 -o trdos.trd` + - `fluxengine read -c trdos --160sssd -s drive:0 -o trdos.trd` + +To write: + + - `fluxengine write -c trdos --640dsdd -d drive:0 -i trdos.trd` + - `fluxengine write -c trdos --320ssdd -d drive:0 -i trdos.trd` + - `fluxengine write -c trdos --320dsdd -d drive:0 -i trdos.trd` + - `fluxengine write -c trdos --160sssd -d drive:0 -i trdos.trd` + diff --git a/java/com/cowlark/fluxengine/arch/trdos.textpb b/java/com/cowlark/fluxengine/arch/trdos.textpb index 068bd358..d59bfd89 100644 --- a/java/com/cowlark/fluxengine/arch/trdos.textpb +++ b/java/com/cowlark/fluxengine/arch/trdos.textpb @@ -105,7 +105,7 @@ option_group { } } option { - name: "160SSDD" + name: "160sssd" comment: '160kB 40-track single-sided' config { diff --git a/javatests/com/cowlark/fluxengine/data/FormatsTest.java b/javatests/com/cowlark/fluxengine/data/FormatsTest.java index 3cd340d3..d256ea96 100644 --- a/javatests/com/cowlark/fluxengine/data/FormatsTest.java +++ b/javatests/com/cowlark/fluxengine/data/FormatsTest.java @@ -35,7 +35,7 @@ public void returnsNullForUnknownName() @Test public void returnsAllConfigNames() { - assertThat(Formats.all()).hasSize(36); + assertThat(Formats.all()).hasSize(37); assertThat(Formats.all()).contains("ibm"); assertThat(Formats.all()).contains("_global_options"); } diff --git a/scripts/corpus.bzl b/scripts/corpus.bzl index 48e396e8..b6f1f168 100644 --- a/scripts/corpus.bzl +++ b/scripts/corpus.bzl @@ -52,6 +52,10 @@ CORPUS = [ ("rx50", "", ""), ("tartu", "", "--390 --drivetype=40"), ("tartu", "", "--780"), + ("trdos", "", "--160sssd"), + ("trdos", "", "--320dsdd"), + ("trdos", "", "--320ssdd"), + ("trdos", "", "--640dsdd"), ("tids990", "", ""), ("victor9k", "", "--612"), ("victor9k", "", "--1224"),