From 634424845aab002008df20e83225aeae9f08b018 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 27 Oct 2026 08:27:42 -0500 Subject: [PATCH 1/3] firmware: the GEOS boot image is built per target, ready for GEOS 128 Each GEOS target now names its own TR disk driver, the start-up runs that set its date and time, and where its boot file keeps the C128 reset key. GeosBuildBootImage reads the boot file, swaps the transport, breaks the reset key and sets the clock. GeosBuildBootPrg wraps that image in GEOS 64's BASIC stub, as before. GEOS 128 sets the clock and the date in one 44-byte run, behind a store of zero that the patched run takes over to load X. Its 1581 driver keeps NewDisk after ReadBlock. The transport swap no longer requires the routines it replaces to lie inside the run it overwrites, since one left outside is only unused. The checks that nothing it leaves alone points into the run stay. CONFIGURE is turned off for any auto-exec file whose name contains CONFIG, which catches GEOS 128's "128 CONFIGURE", "128 CONFIG 2.0r" and "64 CONFIG 2.0r". GEOS 64's boot PRG and patched disk are byte-identical to before on the stock GEOS 64 D81. --- Source/Teensy/GEOS/GeosBoot.h | 249 +++++++++++++----- Source/Teensy/GEOS/GeosDiskPatch.h | 4 + Source/Teensy/GEOS/GeosTargets.h | 15 +- Source/Teensy/GeosLauncher.ino | 5 +- Source/Teensy/tests/geos-boot.test.js | 121 ++++++++- Source/Teensy/tests/geos_boot_harness.c | 39 ++- Source/Teensy/tests/geos_disk_patch_harness.c | 2 +- 7 files changed, 339 insertions(+), 96 deletions(-) diff --git a/Source/Teensy/GEOS/GeosBoot.h b/Source/Teensy/GEOS/GeosBoot.h index 9d596509..fcc207bf 100644 --- a/Source/Teensy/GEOS/GeosBoot.h +++ b/Source/Teensy/GEOS/GeosBoot.h @@ -3,7 +3,6 @@ // the TR's, and the disk's own auto-configuration kept from probing a drive that is not there. #pragma once #include "GeosDiskPatch.h" -#include "TRDriver.h" #define GEOS_DRIVER_AREA 0x9000 #define GEOS_DRIVER_SIZE 0xd80 @@ -14,7 +13,7 @@ #define GEOS_JUMP_OPCODE 0x4c #define GEOS_AUTO_EXEC 14 #define GEOS_SYSTEM_FILE 4 -#define GEOS_CONFIG_PREFIX "CONFIG" +#define GEOS_CONFIG_NAME "CONFIG" #define GEOS_BASIC_START 0x0801 #define GEOS_MEMORY_END 0x10000 #define GEOS_MAX_FILE_BLOCKS 3200 @@ -25,6 +24,17 @@ enum GeosDriverVector { VecInitForIO, VecDoneWithIO, VecExitTurbo, VecPurgeTurbo enum GeosDriverJump { JumpSendTSBytes = 6, JumpGetDOSError = 7, JumpReadLink = 9 }; enum TRDriverEntry { EntrySendTSBytes = 10, EntryGetDOSError, EntryReadLink }; +// A TR disk driver as tools/geos/trdrive.py emits it: code assembled at Base, the offsets of the words that hold +// addresses inside it, and the offsets of its routines in TRDriverEntry order +struct GeosDiskDriver +{ + const uint8_t *Code; + uint16_t Size, Base; + const uint16_t *Relocations; + uint16_t RelocationCount; + const uint16_t *Entries; +}; + static uint16_t GeosWord(const uint8_t *At) { return At[0] | At[1] << 8; } static void GeosPutWord(uint8_t *At, uint16_t Value) @@ -63,19 +73,15 @@ static int32_t GeosFindDriver(const uint8_t *Data, uint32_t Size) return -1; } -// The stock driver's turbo routines occupy one run of its code, from InitForIO up to ReadBlock; the TR's -// driver replaces them and points every vector it implements at its own routines -static bool GeosReplaceTransport(uint8_t *Driver) +// The TR's driver goes over the stock driver's code from InitForIO up to ReadBlock, provided no vector or jump slot +// it leaves alone points there, and every vector it implements is pointed at its own routines. Replaced routines +// outside that run stay where they are, unused: GEOS 128 keeps NewDisk after ReadBlock. +static bool GeosReplaceTransport(uint8_t *Driver, const struct GeosDiskDriver *With) { uint16_t Start = GeosWord(Driver + 2 * VecInitForIO), End = GeosWord(Driver + 2 * VecReadBlock); - uint8_t Code[sizeof TRDriverCode]; + uint8_t *Code = Driver + (Start - GEOS_DRIVER_AREA); - if (End <= Start || (size_t)(End - Start) < sizeof Code) return false; - for (int i = VecDoneWithIO; i <= VecNewDisk; i++) - { - uint16_t Routine = GeosWord(Driver + 2 * i); - if (Routine < Start || Routine >= End) return false; - } + if (End <= Start || (size_t)(End - Start) < With->Size) return false; for (int i = VecCount; i < GEOS_VECTOR_WORDS; i++) { uint16_t Other = GeosWord(Driver + 2 * i); @@ -87,17 +93,16 @@ static bool GeosReplaceTransport(uint8_t *Driver) bool Repointed = i == JumpSendTSBytes || i == JumpGetDOSError || i == JumpReadLink; if (!Repointed && Target >= Start && Target < End) return false; } - memcpy(Code, TRDriverCode, sizeof Code); - for (size_t i = 0; i < sizeof TRDriverRelocations / sizeof TRDriverRelocations[0]; i++) + memcpy(Code, With->Code, With->Size); + for (uint16_t i = 0; i < With->RelocationCount; i++) { - uint8_t *Word = Code + TRDriverRelocations[i]; - GeosPutWord(Word, GeosWord(Word) + Start - TR_DRIVER_BASE); + uint8_t *Word = Code + With->Relocations[i]; + GeosPutWord(Word, GeosWord(Word) + Start - With->Base); } - memcpy(Driver + (Start - GEOS_DRIVER_AREA), Code, sizeof Code); - for (int i = 0; i < VecCount; i++) GeosPutWord(Driver + 2 * i, Start + TRDriverEntries[i]); - GeosPutWord(Driver + GEOS_JUMP_SLOTS + 3 * JumpSendTSBytes + 1, Start + TRDriverEntries[EntrySendTSBytes]); - GeosPutWord(Driver + GEOS_JUMP_SLOTS + 3 * JumpGetDOSError + 1, Start + TRDriverEntries[EntryGetDOSError]); - GeosPutWord(Driver + GEOS_JUMP_SLOTS + 3 * JumpReadLink + 1, Start + TRDriverEntries[EntryReadLink]); + for (int i = 0; i < VecCount; i++) GeosPutWord(Driver + 2 * i, Start + With->Entries[i]); + GeosPutWord(Driver + GEOS_JUMP_SLOTS + 3 * JumpSendTSBytes + 1, Start + With->Entries[EntrySendTSBytes]); + GeosPutWord(Driver + GEOS_JUMP_SLOTS + 3 * JumpGetDOSError + 1, Start + With->Entries[EntryGetDOSError]); + GeosPutWord(Driver + GEOS_JUMP_SLOTS + 3 * JumpReadLink + 1, Start + With->Entries[EntryReadLink]); return true; } @@ -138,9 +143,20 @@ static bool GeosReadFile(struct GeosDisk *Disk, const char *Name, uint8_t *Out, return true; } +static bool GeosNameContains(const uint8_t *Name, const char *Part) +{ + size_t Length = strlen(Part); + + for (size_t i = 0; i + Length <= GEOS_NAME_SIZE; i++) + { + if (!memcmp(Name + i, Part, Length)) return true; + } + return false; +} + // Gives AUTO_EXEC configuration files the type of a file GEOS leaves alone, the way the stock disk's own // configuration probes the IEC bus and empties drive A; returns whether anything was retyped -static bool GeosDisableConfigure(struct GeosDisk *Disk, bool *Changed) +static inline bool GeosDisableConfigure(struct GeosDisk *Disk, bool *Changed) { struct GeosTS Chain[GEOS_MAX_DIR_SECTORS]; uint8_t Block[GEOS_SECTOR_SIZE]; @@ -157,7 +173,7 @@ static bool GeosDisableConfigure(struct GeosDisk *Disk, bool *Changed) { uint8_t *Slot = Block + e * GEOS_DIR_ENTRY_SIZE; if (!(Slot[SlotType] & CBM_TYPE_MASK) || Slot[SlotGeosType] != GEOS_AUTO_EXEC) continue; - if (strncmp((char *)Slot + SlotName, GEOS_CONFIG_PREFIX, strlen(GEOS_CONFIG_PREFIX))) continue; + if (!GeosNameContains(Slot + SlotName, GEOS_CONFIG_NAME)) continue; Slot[SlotGeosType] = GEOS_SYSTEM_FILE; Retyped = true; } @@ -179,19 +195,6 @@ static const uint8_t GeosBootStub[] = 0x4c, 0x00, 0x00, // jmp entry }; -// The kernal's start-up code sets the date with three immediate stores and the clock in CIA 1 with another run; -// both are found in the boot file by their bytes, since the stock file fixes the date and time they set. The -// clock's run follows a loop that leaves X zero, which the patched run uses for the seconds and tenths. -static const uint8_t GeosStartDate[] = { 0xa9, 0x17, 0x8d, 0x18, 0x85, 0xa9, 0x03, 0x8d, 0x17, 0x85, 0xa9, 0x5c, 0x8d, 0x16, 0x85 }; -static const uint8_t GeosStartClock[] = -{ - 0xca, 0xd0, 0xe4, // dex : bne, the loop before it - 0xad, 0x0f, 0xdc, 0x29, 0x7f, 0x8d, 0x0f, 0xdc, // clear the alarm-set bit - 0xa9, 0x81, 0x8d, 0x0b, 0xdc, // hour 1 PM - 0xa9, 0x00, 0x8d, 0x0a, 0xdc, // minute 0 - 0x8d, 0x09, 0xdc, 0x8d, 0x08, 0xdc, // second and tenths 0; the tenths start the clock -}; -#define GEOS_CLOCK_TIME 11 #define GEOS_PM_BIT 0x80 static uint8_t GeosBcd(uint8_t Value) @@ -208,61 +211,171 @@ static uint8_t GeosTodHour(uint8_t Hour) return GeosBcd(Hour12) | (Pm ? GEOS_PM_BIT : 0); } -// Finds Pattern once in Prg; NULL when it is absent or ambiguous -static uint8_t *GeosFindOnce(uint8_t *Prg, uint32_t Length, const uint8_t *Pattern, uint32_t Size) +// One run of a boot file's start-up code that sets the date or the time. It is found by its bytes, since the stock +// file fixes the values it sets, and Set makes it set Now instead without changing its length. +struct GeosClockRun +{ + const uint8_t *Pattern; + uint8_t Size; + void (*Set)(uint8_t *Run, const struct GeosDate *Now); +}; + +// GEOS 64 sets the date with three immediate stores and the clock in CIA 1 with another run. The clock's run +// follows a loop that leaves X zero, which the patched run uses for the seconds and tenths. +static const uint8_t Geos64StartDate[] = { 0xa9, 0x17, 0x8d, 0x18, 0x85, 0xa9, 0x03, 0x8d, 0x17, 0x85, 0xa9, 0x5c, 0x8d, 0x16, 0x85 }; +static const uint8_t Geos64StartClock[] = +{ + 0xca, 0xd0, 0xe4, // dex : bne, the loop before it + 0xad, 0x0f, 0xdc, 0x29, 0x7f, 0x8d, 0x0f, 0xdc, // clear the alarm-set bit + 0xa9, 0x81, 0x8d, 0x0b, 0xdc, // hour 1 PM + 0xa9, 0x00, 0x8d, 0x0a, 0xdc, // minute 0 + 0x8d, 0x09, 0xdc, 0x8d, 0x08, 0xdc, // second and tenths 0; the tenths start the clock +}; +#define GEOS64_CLOCK_TIME 11 + +static void Geos64SetDate(uint8_t *Run, const struct GeosDate *Now) +{ + Run[1] = Now->Day; + Run[6] = Now->Month; + Run[11] = Now->Year; +} + +static void Geos64SetTime(uint8_t *Run, const struct GeosDate *Now) +{ + const uint8_t Time[] = + { + 0xa9, GeosTodHour(Now->Hour), 0x8d, 0x0b, 0xdc, + 0xa9, GeosBcd(Now->Minute), 0x8d, 0x0a, 0xdc, + 0x8e, 0x09, 0xdc, 0x8e, 0x08, 0xdc, + }; + + memcpy(Run + GEOS64_CLOCK_TIME, Time, sizeof Time); +} + +static const struct GeosClockRun Geos64ClockRuns[] = +{ + { Geos64StartDate, sizeof Geos64StartDate, Geos64SetDate }, + { Geos64StartClock, sizeof Geos64StartClock, Geos64SetTime }, +}; + +// GEOS 128 sets the clock and then the date in one run, behind a store of zero. No register is known to be zero +// there and the run has no byte to spare for clearing one, so the patched run makes that store load X instead. +static const uint8_t Geos128StartUp[] = +{ + 0xa9, 0x00, 0x8d, 0xc5, 0x88, // lda #0 : sta $88c5 + 0xad, 0x0f, 0xdc, 0x29, 0x7f, 0x8d, 0x0f, 0xdc, // clear the alarm-set bit + 0xa9, 0x81, 0x8d, 0x0b, 0xdc, // hour 1 PM + 0xa9, 0x00, 0x8d, 0x0a, 0xdc, // minute 0 + 0x8d, 0x09, 0xdc, 0x8d, 0x08, 0xdc, // second and tenths 0; the tenths start the clock + 0xa9, 0x5c, 0x8d, 0x16, 0x85, // year 92 + 0xa9, 0x03, 0x8d, 0x17, 0x85, // month 3 + 0xa9, 0x17, 0x8d, 0x18, 0x85, // day 23 +}; + +static void Geos128SetClock(uint8_t *Run, const struct GeosDate *Now) +{ + const uint8_t StartUp[sizeof Geos128StartUp] = + { + 0xa2, 0x00, 0x8e, 0xc5, 0x88, + 0xad, 0x0f, 0xdc, 0x29, 0x7f, 0x8d, 0x0f, 0xdc, + 0xa9, GeosTodHour(Now->Hour), 0x8d, 0x0b, 0xdc, + 0xa9, GeosBcd(Now->Minute), 0x8d, 0x0a, 0xdc, + 0x8e, 0x09, 0xdc, 0x8e, 0x08, 0xdc, + 0xa9, Now->Year, 0x8d, 0x16, 0x85, + 0xa9, Now->Month, 0x8d, 0x17, 0x85, + 0xa9, Now->Day, 0x8d, 0x18, 0x85, + }; + + memcpy(Run, StartUp, sizeof StartUp); +} + +static const struct GeosClockRun Geos128ClockRuns[] = { { Geos128StartUp, sizeof Geos128StartUp, Geos128SetClock } }; + +// Finds Pattern once in Body; NULL when it is absent or ambiguous +static uint8_t *GeosFindOnce(uint8_t *Body, uint32_t Length, const uint8_t *Pattern, uint32_t Size) { uint8_t *Found = NULL; for (uint32_t i = 0; i + Size <= Length; i++) { - if (memcmp(Prg + i, Pattern, Size)) continue; + if (memcmp(Body + i, Pattern, Size)) continue; if (Found) return NULL; - Found = Prg + i; + Found = Body + i; } return Found; } -// Sets the date and time GEOS starts with. Leaves the PRG as it is, and returns false, for a boot file whose -// start-up code is not the one this knows -static bool GeosSetClock(uint8_t *Prg, uint32_t Length, const struct GeosDate *Now) +// Sets the date and time GEOS starts with. Leaves the body as it is, and returns false, for a boot file whose +// start-up code is not the one the target knows +static bool GeosSetClock(uint8_t *Body, uint32_t Length, const struct GeosTarget *Target, const struct GeosDate *Now) { - uint8_t *Date = GeosFindOnce(Prg, Length, GeosStartDate, sizeof GeosStartDate); - uint8_t *Clock = GeosFindOnce(Prg, Length, GeosStartClock, sizeof GeosStartClock); - uint8_t Time[] = + for (uint8_t i = 0; i < Target->ClockRunCount; i++) { - 0xa9, GeosTodHour(Now->Hour), 0x8d, 0x0b, 0xdc, - 0xa9, GeosBcd(Now->Minute), 0x8d, 0x0a, 0xdc, - 0x8e, 0x09, 0xdc, 0x8e, 0x08, 0xdc, - }; + const struct GeosClockRun *Run = &Target->ClockRuns[i]; + if (!GeosFindOnce(Body, Length, Run->Pattern, Run->Size)) return false; + } + for (uint8_t i = 0; i < Target->ClockRunCount; i++) + { + const struct GeosClockRun *Run = &Target->ClockRuns[i]; + Run->Set(GeosFindOnce(Body, Length, Run->Pattern, Run->Size), Now); + } + return true; +} + +// GEOS 128 leaves "CBM" and $03E4 in bank 1 at $FFF5, and the C128 kernal resumes there on every reset until the +// power goes off. The TR menu runs in C64 mode and overwrites $03E4, so the next C128 program would crash. +static const uint8_t GeosResetKey[] = { 'C', 'B', 'M', 0xe4, 0x03 }; - if (!Date || !Clock) return false; - Date[1] = Now->Day; - Date[6] = Now->Month; - Date[11] = Now->Year; - memcpy(Clock + GEOS_CLOCK_TIME, Time, sizeof Time); +static bool GeosBreakResetKey(struct GeosDisk *Disk, uint8_t *Image, uint32_t Length) +{ + uint16_t Key = Disk->Target->ResetKey, Load = GeosWord(Image); + uint32_t At = 2 + (uint32_t)(Key - Load); + + if (!Key) return true; + if (Key < Load || At + sizeof GeosResetKey > Length || memcmp(Image + At, GeosResetKey, sizeof GeosResetKey)) + { + return GeosFail(Disk, PSTR("the boot file's reset key is not where this version keeps it")); + } + Image[At] = 0; return true; } -// One PRG that starts GEOS: the boot file with the disk driver's transport replaced, placed where its load -// address puts it, behind a BASIC line that jumps to the boot entry -static bool GeosBuildBootPrg(struct GeosDisk *Disk, uint8_t *Out, uint32_t Capacity, uint32_t *Length) +// The boot file as GEOS starts from it, load address first: the disk driver's transport replaced, any reset key +// broken, and the clock set to Now where the start-up code is one the target knows +static inline bool GeosBuildBootImage(struct GeosDisk *Disk, const struct GeosDate *Now, uint8_t *Out, uint32_t Capacity, + uint32_t *Length, bool *ClockSet) { - const uint32_t Prologue = 2 + sizeof GeosBasicLine + sizeof GeosBootStub; - uint32_t FileSize, BodySize, Load, Place; + const struct GeosTarget *Target = Disk->Target; int32_t Driver; - uint8_t *Body = Out + 2; - if (!Disk->Target->BootFile) return GeosFail(Disk, PSTR("this version has no boot file to start from")); - if (!GeosReadFile(Disk, Disk->Target->BootFile, Out, Capacity, &FileSize)) return false; - if (FileSize < 2 + GEOS_DRIVER_SIZE) return GeosFail(Disk, PSTR("the boot file is too short")); - BodySize = FileSize - 2; + if (!Target->BootFile) return GeosFail(Disk, PSTR("this version has no boot file to start from")); + if (!GeosReadFile(Disk, Target->BootFile, Out, Capacity, Length)) return false; + if (*Length < 2 + GEOS_DRIVER_SIZE) return GeosFail(Disk, PSTR("the boot file is too short")); + Driver = GeosFindDriver(Out + 2, *Length - 2); + if (Driver < 0 || !GeosReplaceTransport(Out + 2 + Driver, Target->DiskDriver)) + { + return GeosFail(Disk, PSTR("the boot file's disk driver is not one the TR can replace")); + } + if (!GeosBreakResetKey(Disk, Out, *Length)) return false; + *ClockSet = GeosSetClock(Out + 2, *Length - 2, Target, Now); + return true; +} + +// One PRG that starts GEOS 64: the boot image placed where its load address puts it, behind a BASIC line that +// jumps to the boot entry +static inline bool GeosBuildBootPrg(struct GeosDisk *Disk, const struct GeosDate *Now, uint8_t *Out, uint32_t Capacity, + uint32_t *Length, bool *ClockSet) +{ + const uint32_t Prologue = 2 + sizeof GeosBasicLine + sizeof GeosBootStub; + uint32_t ImageSize, BodySize, Load, Place; + + if (!GeosBuildBootImage(Disk, Now, Out, Capacity, &ImageSize, ClockSet)) return false; + BodySize = ImageSize - 2; Load = GeosWord(Out); - Driver = GeosFindDriver(Body, BodySize); - if (Driver < 0 || !GeosReplaceTransport(Body + Driver)) return GeosFail(Disk, PSTR("the boot file's disk driver is not one the TR can replace")); Place = 2 + Load - GEOS_BASIC_START; if (Load < GEOS_BASIC_START + Prologue - 2 || Place + BodySize > Capacity || Load + BodySize > GEOS_MEMORY_END) return GeosFail(Disk, PSTR("the boot file does not fit in memory")); - memmove(Out + Place, Body, BodySize); + memmove(Out + Place, Out + 2, BodySize); memset(Out, 0, Place); GeosPutWord(Out, GEOS_BASIC_START); memcpy(Out + 2, GeosBasicLine, sizeof GeosBasicLine); diff --git a/Source/Teensy/GEOS/GeosDiskPatch.h b/Source/Teensy/GEOS/GeosDiskPatch.h index 35926627..ad827138 100644 --- a/Source/Teensy/GEOS/GeosDiskPatch.h +++ b/Source/Teensy/GEOS/GeosDiskPatch.h @@ -64,6 +64,10 @@ struct GeosTarget uint8_t InputType; const char *BootFile; // what the TR starts the disk from uint16_t BootEntry; + const struct GeosDiskDriver *DiskDriver; + const struct GeosClockRun *ClockRuns; + uint8_t ClockRunCount; + uint16_t ResetKey; // where the boot file holds the C128's reset key, or 0 }; struct GeosBootDisk { bool HasDeskTop, HasDriver; }; diff --git a/Source/Teensy/GEOS/GeosTargets.h b/Source/Teensy/GEOS/GeosTargets.h index c96b9f46..7bc7e8a3 100644 --- a/Source/Teensy/GEOS/GeosTargets.h +++ b/Source/Teensy/GEOS/GeosTargets.h @@ -1,22 +1,35 @@ // SPDX-License-Identifier: MIT // The GEOS versions the TR sets up from a boot disk image the user puts on the SD card. #pragma once +#include "GeosBoot.h" #include "GeosDiskPatch.h" +#include "TRDriver.h" #include "TRMouse64.h" #define GEOS_INPUT_64 10 #define GEOS64_BOOT_FILE "GEOBOOT" #define GEOS64_BOOT_ENTRY 0x2f3c +#define GEOS128_BOOT_FILE "GEOBOOT128" +#define GEOS128_BOOT_ENTRY 0x2f3c +#define GEOS128_RESET_KEY 0x6ff5 + struct GeosLauncher { const char *Title, *Folder; struct GeosTarget Target; }; +static const struct GeosDiskDriver TRDiskDriver64 = +{ + TRDriverCode, sizeof TRDriverCode, TR_DRIVER_BASE, + TRDriverRelocations, sizeof TRDriverRelocations / sizeof TRDriverRelocations[0], TRDriverEntries, +}; + static const struct GeosLauncher Geos64Launcher = { "GEOS 64", "/GEOS64", { "DESK TOP", TRMouse64Name, TRMouse64Code, TRMouse64Info, sizeof TRMouse64Code, GEOS_INPUT_64, - GEOS64_BOOT_FILE, GEOS64_BOOT_ENTRY }, + GEOS64_BOOT_FILE, GEOS64_BOOT_ENTRY, &TRDiskDriver64, + Geos64ClockRuns, sizeof Geos64ClockRuns / sizeof Geos64ClockRuns[0], 0 }, }; diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index b2278fb7..d4021e2d 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -263,7 +263,7 @@ FLASHMEM static bool GeosStart(const struct GeosLauncher *Launcher, const char * struct GeosDisk Disk; uint32_t Length = 0; struct GeosDate Now = GeosToday(); - bool Changed, Built; + bool Changed, ClockSet, Built; File Image; GeosPath(Path, sizeof Path, Launcher, Name); @@ -274,8 +274,7 @@ FLASHMEM static bool GeosStart(const struct GeosLauncher *Launcher, const char * return false; } Disk = GeosDiskOn(Launcher, &Image); - Built = GeosBuildBootPrg(&Disk, RAM_Image, RAM_ImageSize, &Length) && GeosDisableConfigure(&Disk, &Changed); - if (Built) GeosSetClock(RAM_Image, Length, &Now); + Built = GeosBuildBootPrg(&Disk, &Now, RAM_Image, RAM_ImageSize, &Length, &ClockSet) && GeosDisableConfigure(&Disk, &Changed); if (!Built) SendMsgPrintfln(PSTR("%s:\r\n%s"), Name, Disk.Error); Image.close(); if (!Built) return false; diff --git a/Source/Teensy/tests/geos-boot.test.js b/Source/Teensy/tests/geos-boot.test.js index f919f1ea..7001a022 100644 --- a/Source/Teensy/tests/geos-boot.test.js +++ b/Source/Teensy/tests/geos-boot.test.js @@ -66,6 +66,14 @@ function stockDriver() { return driver; } +// GEOS 128's 1581 driver keeps NewDisk after ReadBlock, outside the run the TR driver takes +const NEW_DISK_128 = 0x9480; +function stockDriver128() { + const driver = stockDriver(); + driver.writeUInt16LE(NEW_DISK_128, 2 * 6); + return driver; +} + const START_AT_IN_BODY = 0x1000; const START_DATE = [0xa9, 0x17, 0x8d, 0x18, 0x85, 0xa9, 0x03, 0x8d, 0x17, 0x85, 0xa9, 0x5c, 0x8d, 0x16, 0x85]; const LOOP_END = [0xca, 0xd0, 0xe4]; @@ -79,15 +87,16 @@ const TOD_TENTHS = 0xdc08; const TOD_HOURS = 0xdc0b; const PM = 0x80; -// Runs the start-up clock code the way a 6526 takes it: X is zero from the loop before it, writes go to the alarm -// while CRB selects it, and writing hour 12 flips AM/PM. Returns the TOD registers it sets, tenths first. -function runClock(code) { +// Runs the start-up clock code the way a 6526 takes it: X is zero from the loop before it unless given, writes go +// to the alarm while CRB selects it, and writing hour 12 flips AM/PM. Returns the TOD registers it sets, tenths +// first; other stores land in memory. +function runClock(code, { x = 0, memory = {} } = {}) { let a = 0; - let x = 0; let crb = ALARM_SELECT; const tod = []; const write = (address, value) => { if (address === CRB) crb = value; + else if (address < TOD_TENTHS || address > TOD_HOURS) memory[address] = value; else if (crb & ALARM_SELECT) assert.fail(`$${address.toString(16)} written while CRB selects the alarm`); else tod[address - TOD_TENTHS] = address === TOD_HOURS && (value & 0x1f) === 0x12 ? value ^ PM : value; }; @@ -105,13 +114,16 @@ function runClock(code) { return tod; } -function bootFile(driver = stockDriver(), startUp = []) { +const GEOS64 = { load: LOAD_ADDRESS, driverAt: DRIVER_AT_IN_BODY, startUpAt: START_AT_IN_BODY }; + +function bootFile(driver = stockDriver(), startUp = [], layout = GEOS64) { const body = Buffer.alloc(BODY_SIZE); for (let i = 0; i < BODY_SIZE; i++) body[i] = (i * 31 + 7) & 0xff; - driver.copy(body, DRIVER_AT_IN_BODY); - Buffer.from(startUp).copy(body, START_AT_IN_BODY); + driver.copy(body, layout.driverAt); + Buffer.from(startUp).copy(body, layout.startUpAt); + if (layout.resetKey) Buffer.from(layout.resetKey.bytes).copy(body, layout.resetKey.at); const file = Buffer.alloc(2 + BODY_SIZE); - file.writeUInt16LE(LOAD_ADDRESS, 0); + file.writeUInt16LE(layout.load, 0); body.copy(file, 2); return { file, body }; } @@ -164,9 +176,9 @@ function withHarness(body) { assert.equal(built.status, 0, built.err); const asCpp = run(['c++', '-x', 'c++', '-std=gnu++17', '-Wall', '-Werror', '-fsyntax-only', HARNESS_SOURCE]); assert.equal(asCpp.status, 0, asCpp.err); - const launch = (disk, time = []) => { + const launch = (disk, time = [], version = '64') => { fs.writeFileSync(path.join(dir, 'in.d81'), disk.image); - const r = run([path.join(dir, 'harness'), path.join(dir, 'in.d81'), path.join(dir, 'out.prg'), path.join(dir, 'out.d81'), ...time.map(String)]); + const r = run([path.join(dir, 'harness'), version, path.join(dir, 'in.d81'), path.join(dir, 'out.prg'), path.join(dir, 'out.d81'), ...time.map(String)]); return { ...r, prg: r.status === 0 ? fs.readFileSync(path.join(dir, 'out.prg')) : null, image: r.status === 0 ? fs.readFileSync(path.join(dir, 'out.d81')) : null }; }; body(launch); @@ -182,6 +194,29 @@ function standardDisk(driver, startUp = []) { return disk; } +// GEOBOOT128 as the stock GEOS 128 2.0 disk has it: loaded at $2B19, the 1581 driver staged at $7000 with the +// reset key just below it, and the clock and date set in one run at $2C24 +const GEOS128_LOAD = 0x2b19; +const RESET_KEY = [0x43, 0x42, 0x4d, 0xe4, 0x03]; +const START_UP_128 = [ + 0xa9, 0x00, 0x8d, 0xc5, 0x88, ...START_CLOCK, + 0xa9, 0x5c, 0x8d, 0x16, 0x85, 0xa9, 0x03, 0x8d, 0x17, 0x85, 0xa9, 0x17, 0x8d, 0x18, 0x85, +]; +const GEOS128 = { + load: GEOS128_LOAD, + driverAt: 0x7000 - GEOS128_LOAD, + startUpAt: 0x2c24 - GEOS128_LOAD, + resetKey: { at: 0x6ff5 - GEOS128_LOAD, bytes: RESET_KEY }, +}; +const DATE = { 0x8516: 26, 0x8517: 10, 0x8518: 21 }; + +function geos128Disk(startUp = START_UP_128, layout = GEOS128) { + const disk = new Disk(); + disk.addFile('GEOS128', Buffer.from([1, 8, 0])); + disk.addFile('GEOBOOT128', bootFile(stockDriver128(), startUp, layout).file); + return disk; +} + test('the boot PRG carries the boot file, with the TR disk driver in place of the stock transport', { skip }, () => { withHarness((launch) => { const r = launch(standardDisk()); @@ -281,6 +316,7 @@ test('CONFIGURE files stop being auto-executed, and nothing else changes', { ski const disk = standardDisk(); const configure = disk.addFile('CONFIGURE', Buffer.from([1, 2, 3]), AUTO_EXEC); const second = disk.addFile('CONFIG 2.0r', Buffer.from([1, 2, 3]), AUTO_EXEC); + const geos128 = ['128 CONFIGURE', '128 CONFIG 2.0r', '64 CONFIG 2.0r'].map((name) => disk.addFile(name, Buffer.from([1, 2, 3]), AUTO_EXEC)); const other = disk.addFile('pad color mgr', Buffer.from([1, 2, 3]), AUTO_EXEC); const plain = disk.addFile('CONFIGURE.DOC', Buffer.from([1, 2, 3]), 0); @@ -289,6 +325,7 @@ test('CONFIGURE files stop being auto-executed, and nothing else changes', { ski assert.match(first.out, /changed=1/); assert.equal(first.image[configure + SLOT_GEOS_TYPE], SYSTEM_FILE); assert.equal(first.image[second + SLOT_GEOS_TYPE], SYSTEM_FILE); + for (const slot of geos128) assert.equal(first.image[slot + SLOT_GEOS_TYPE], SYSTEM_FILE, 'GEOS 128 names CONFIG after a prefix'); assert.equal(first.image[other + SLOT_GEOS_TYPE], AUTO_EXEC, 'other auto-exec files stay'); assert.equal(first.image[plain + SLOT_GEOS_TYPE], 0, 'a CONFIG file that is not auto-exec stays'); @@ -297,3 +334,67 @@ test('CONFIGURE files stop being auto-executed, and nothing else changes', { ski assert.equal(again.image.equals(first.image), true); }); }); + +test('the GEOS 128 boot image is the boot file with the TR disk driver in place and the reset key broken', { skip }, () => { + withHarness((launch) => { + const r = launch(geos128Disk(), [], '128'); + assert.equal(r.status, 0, r.err); + const { file } = bootFile(stockDriver128(), START_UP_128, GEOS128); + const driverAt = 2 + GEOS128.driverAt; + const keyAt = 2 + GEOS128.resetKey.at; + const startAt = 2 + GEOS128.startUpAt; + + assert.equal(r.prg.length, file.length); + assert.equal(r.prg.readUInt16LE(0), GEOS128_LOAD); + const patched = r.prg.subarray(driverAt, driverAt + DRIVER_SIZE); + for (let i = 0; i < 10; i++) assert.equal(patched.readUInt16LE(2 * i), TURBO_START + ENTRIES[i], `vector ${i}`); + const oldNewDisk = NEW_DISK_128 - DRIVER_AREA; + assert.equal(patched.subarray(oldNewDisk, READ_BLOCK - DRIVER_AREA + 0x100).equals(stockDriver128().subarray(oldNewDisk, READ_BLOCK - DRIVER_AREA + 0x100)), true, 'the stock NewDisk stays'); + assert.deepEqual([...r.prg.subarray(keyAt, keyAt + RESET_KEY.length)], [0, ...RESET_KEY.slice(1)], 'the C of CBM is gone'); + + const unchanged = (from, to) => assert.equal(r.prg.subarray(from, to).equals(file.subarray(from, to)), true, `bytes ${from}-${to}`); + unchanged(0, startAt); + unchanged(startAt + START_UP_128.length, keyAt); + unchanged(keyAt + 1, driverAt); + unchanged(driverAt + DRIVER_SIZE, file.length); + }); +}); + +test('GEOS 128 starts with the given date and time, whatever X holds', { skip }, () => { + withHarness((launch) => { + const startAt = 2 + GEOS128.startUpAt; + for (const [hour, minute, held] of [[0, 5, 0x12], [12, 30, PM | 0x12], [23, 59, PM | 0x11], [15, 7, PM | 0x03]]) { + const r = launch(geos128Disk(), [hour, minute], '128'); + assert.match(r.out, /clock=1/); + const memory = {}; + const tod = runClock(r.prg.subarray(startAt, startAt + START_UP_128.length), { x: 0x5a, memory }); + assert.deepEqual(tod, [0x00, 0x00, parseInt(String(minute), 16), held], `${hour}:${minute}`); + assert.deepEqual(memory, { 0x88c5: 0, ...DATE }, 'the store before the run, then year, month, day'); + } + }); +}); + +test('a GEOS 128 boot file without its start-up code keeps the date and time it has', { skip }, () => { + withHarness((launch) => { + const startAt = 2 + GEOS128.startUpAt; + for (const startUp of [START_UP_128.slice(5), START_UP]) { + const r = launch(geos128Disk(startUp), [], '128'); + assert.equal(r.status, 0, r.err); + assert.match(r.out, /clock=0/, 'GEOS 64\'s runs are not GEOS 128\'s'); + assert.deepEqual([...r.prg.subarray(startAt, startAt + startUp.length)], startUp); + } + }); +}); + +test('a GEOS 128 boot file whose reset key is not where GEOS 128 keeps it is refused', { skip }, () => { + withHarness((launch) => { + for (const bytes of [[0x43, 0x42, 0x4d, 0xe5, 0x03], [0, 0, 0, 0, 0]]) { + const r = launch(geos128Disk(START_UP_128, { ...GEOS128, resetKey: { at: GEOS128.resetKey.at, bytes } }), [], '128'); + assert.equal(r.status, 1); + assert.match(r.err, /reset key is not where this version keeps it/); + } + const early = launch(geos128Disk(START_UP_128, { ...GEOS128, load: 0x7000 }), [], '128'); + assert.equal(early.status, 1, 'a boot file loaded above the key'); + assert.match(early.err, /reset key/); + }); +}); diff --git a/Source/Teensy/tests/geos_boot_harness.c b/Source/Teensy/tests/geos_boot_harness.c index 24b75d57..0d05248f 100644 --- a/Source/Teensy/tests/geos_boot_harness.c +++ b/Source/Teensy/tests/geos_boot_harness.c @@ -1,13 +1,23 @@ // SPDX-License-Identifier: MIT -// geos_boot_harness IN.d81 OUT.prg OUT.d81 [HOUR MINUTE]: builds the GEOS 64 boot PRG from the disk, retypes its -// CONFIGURE files, sets the clock to 2026-10-21 at HOUR:MINUTE (15:07 if not given), and writes both. Prints "changed=<0|1> clock=<0|1>"; exits 1 with the refusal on stderr. +// geos_boot_harness 64|128 IN.d81 OUT OUT.d81 [HOUR MINUTE]: builds the GEOS 64 boot PRG, or the GEOS 128 boot +// image, from the disk, retypes its CONFIGURE files, sets the clock to 2026-10-21 at HOUR:MINUTE (15:07 if not +// given), and writes both. Prints "changed=<0|1> clock=<0|1>"; exits 1 with the refusal on stderr. #include -#include "../GEOS/GeosBoot.h" #include "../GEOS/GeosTargets.h" +#include "../GEOS/TRMouse128.h" #define MAX_IMAGE 819200 #define MAX_PRG (128 * 1024) +#define GEOS_INPUT_128 15 + +// GEOS 128's boot facts, on GEOS 64's disk driver until there is a 128 one: the transport swap is the same +static const struct GeosTarget Geos128Boot = +{ + "128 DESKTOP", TRMouse128Name, TRMouse128Code, TRMouse128Info, sizeof TRMouse128Code, GEOS_INPUT_128, + GEOS128_BOOT_FILE, GEOS128_BOOT_ENTRY, &TRDiskDriver64, + Geos128ClockRuns, sizeof Geos128ClockRuns / sizeof Geos128ClockRuns[0], GEOS128_RESET_KEY, +}; static uint8_t Image[MAX_IMAGE + 1], Prg[MAX_PRG]; @@ -28,34 +38,37 @@ int main(int argc, char **argv) struct GeosDate Now = { 26, 10, 21, 15, 7 }; struct GeosDisk Disk = { 0 }; uint32_t Length; - bool Changed; + bool Is128, Changed, ClockSet, Built; FILE *File; size_t Size; - if ((argc != 4 && argc != 6) || !(File = fopen(argv[1], "rb"))) return 2; - if (argc == 6) + if ((argc != 5 && argc != 7) || !(File = fopen(argv[2], "rb"))) return 2; + Is128 = !strcmp(argv[1], "128"); + if (argc == 7) { - Now.Hour = atoi(argv[4]); - Now.Minute = atoi(argv[5]); + Now.Hour = atoi(argv[5]); + Now.Minute = atoi(argv[6]); } Size = fread(Image, 1, sizeof Image, File); fclose(File); Disk.Format = GeosFormatForSize(Size); - Disk.Target = &Geos64Launcher.Target; + Disk.Target = Is128 ? &Geos128Boot : &Geos64Launcher.Target; Disk.Context = Image; Disk.Read = ReadBlock; Disk.Write = WriteBlock; - if (!GeosBuildBootPrg(&Disk, Prg, sizeof Prg, &Length) || !GeosDisableConfigure(&Disk, &Changed)) + Built = Is128 ? GeosBuildBootImage(&Disk, &Now, Prg, sizeof Prg, &Length, &ClockSet) + : GeosBuildBootPrg(&Disk, &Now, Prg, sizeof Prg, &Length, &ClockSet); + if (!Built || !GeosDisableConfigure(&Disk, &Changed)) { fprintf(stderr, "%s\n", Disk.Error); return 1; } - printf("changed=%d clock=%d\n", Changed, GeosSetClock(Prg, Length, &Now)); - File = fopen(argv[2], "wb"); + printf("changed=%d clock=%d\n", Changed, ClockSet); + File = fopen(argv[3], "wb"); fwrite(Prg, 1, Length, File); fclose(File); - File = fopen(argv[3], "wb"); + File = fopen(argv[4], "wb"); fwrite(Image, 1, Size, File); fclose(File); return 0; diff --git a/Source/Teensy/tests/geos_disk_patch_harness.c b/Source/Teensy/tests/geos_disk_patch_harness.c index 88b5f30e..3a3d2213 100644 --- a/Source/Teensy/tests/geos_disk_patch_harness.c +++ b/Source/Teensy/tests/geos_disk_patch_harness.c @@ -27,7 +27,7 @@ static bool WriteBlock(void *Context, uint32_t Offset, const uint8_t *Block) } static const struct GeosTarget Geos128Target = - { "128 DESKTOP", TRMouse128Name, TRMouse128Code, TRMouse128Info, sizeof TRMouse128Code, GEOS_INPUT_128, NULL, 0 }; + { "128 DESKTOP", TRMouse128Name, TRMouse128Code, TRMouse128Info, sizeof TRMouse128Code, GEOS_INPUT_128, NULL, 0, NULL, NULL, 0, 0 }; static int Inspect(struct GeosDisk *Disk) { From 5e1035170305c828f379552bd8d569b9dc3a2b97 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 27 Oct 2026 08:34:18 -0500 Subject: [PATCH 2/3] fix: review findings for 6344248 --- Source/Teensy/tests/geos-boot.test.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/Teensy/tests/geos-boot.test.js b/Source/Teensy/tests/geos-boot.test.js index 7001a022..6174b16c 100644 --- a/Source/Teensy/tests/geos-boot.test.js +++ b/Source/Teensy/tests/geos-boot.test.js @@ -396,5 +396,8 @@ test('a GEOS 128 boot file whose reset key is not where GEOS 128 keeps it is ref const early = launch(geos128Disk(START_UP_128, { ...GEOS128, load: 0x7000 }), [], '128'); assert.equal(early.status, 1, 'a boot file loaded above the key'); assert.match(early.err, /reset key/); + const short = launch(geos128Disk(START_UP_128, { ...GEOS128, load: 0x0f00 }), [], '128'); + assert.equal(short.status, 1, 'a boot file that ends before the key'); + assert.match(short.err, /reset key/); }); }); From f224c24d81047f6fe5726e79214979ea93910ff6 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 29 Oct 2026 13:03:42 -0500 Subject: [PATCH 3/3] Apply suggestions from code review Travis's review of #75: the GEOS 64 clock helpers keep FLASHMEM now that they are reached through function pointers. --- Source/Teensy/GEOS/GeosBoot.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Teensy/GEOS/GeosBoot.h b/Source/Teensy/GEOS/GeosBoot.h index fcc207bf..d54af67c 100644 --- a/Source/Teensy/GEOS/GeosBoot.h +++ b/Source/Teensy/GEOS/GeosBoot.h @@ -233,14 +233,14 @@ static const uint8_t Geos64StartClock[] = }; #define GEOS64_CLOCK_TIME 11 -static void Geos64SetDate(uint8_t *Run, const struct GeosDate *Now) +FLASHMEM static void Geos64SetDate(uint8_t *Run, const struct GeosDate *Now) { Run[1] = Now->Day; Run[6] = Now->Month; Run[11] = Now->Year; } -static void Geos64SetTime(uint8_t *Run, const struct GeosDate *Now) +FLASHMEM static void Geos64SetTime(uint8_t *Run, const struct GeosDate *Now) { const uint8_t Time[] = {