diff --git a/Source/Teensy/GEOS/GeosBoot.h b/Source/Teensy/GEOS/GeosBoot.h index 9d596509..d54af67c 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 + +FLASHMEM static void Geos64SetDate(uint8_t *Run, const struct GeosDate *Now) +{ + Run[1] = Now->Day; + Run[6] = Now->Month; + Run[11] = Now->Year; +} + +FLASHMEM 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..6174b16c 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,70 @@ 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/); + 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/); + }); +}); 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) {