diff --git a/.gitattributes b/.gitattributes index ebeba5c5..84883f63 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,3 +4,6 @@ # Generated by tools/build-c64.mjs with LF endings; check them out as LF too so a # rebuild on Windows (core.autocrlf=true) doesn't flag unchanged headers as modified Source/Teensy/TRMenuFiles/ROMs/*.h text eol=lf + +# Generated by tools/geos/trmouse.py --c-header with LF endings, kept LF for the same reason +Source/Teensy/GEOS/TRMouse128.h text eol=lf diff --git a/Source/C64/MainMenuCRT/source/Menu_Regs.i b/Source/C64/MainMenuCRT/source/Menu_Regs.i index 57d5b5df..fecb5b32 100644 --- a/Source/C64/MainMenuCRT/source/Menu_Regs.i +++ b/Source/C64/MainMenuCRT/source/Menu_Regs.i @@ -394,6 +394,7 @@ rtBinC128 = 18 rtFileREU = 19 rtFileTRH = 20 + rtGEOS = 21 ;63 max: rRegItemTypePlusIOH uses bit 7 (assigned IOH) and bit 6 (/VMS claim), ;and TblItemType's mult by 4 limits it to 63 as well diff --git a/Source/C64/MainMenuCRT/source/StringsMsgs.s b/Source/C64/MainMenuCRT/source/StringsMsgs.s index 44956125..3d4895d0 100644 --- a/Source/C64/MainMenuCRT/source/StringsMsgs.s +++ b/Source/C64/MainMenuCRT/source/StringsMsgs.s @@ -177,3 +177,4 @@ TblItemType: ;must match regItemTypes (rtNone, rtBin16k, etc) order! !tx 0, "128" ; rtBinC128 = 18 !tx 0, "REU" ; rtFileREU = 19 !tx 0, "TRH" ; rtFileTRH = 20 + !tx 0, "GEO" ; rtGEOS = 21 diff --git a/Source/Teensy/DriveDirLoad.ino b/Source/Teensy/DriveDirLoad.ino index d3e7be7e..33a8f2ce 100644 --- a/Source/Teensy/DriveDirLoad.ino +++ b/Source/Teensy/DriveDirLoad.ino @@ -270,6 +270,12 @@ FLASHMEM void HandleExecution() return; } + if (MenuSelCpy.ItemType == rtGEOS) + { + LaunchGeos((const struct GeosLauncher*)MenuSelCpy.Code_Image); + return; + } + SendMsgPrintfln("%s", MenuSelCpy.Name); if (MenuSelCpy.ItemType == rtFileCrt) { //load the CRT into RAM diff --git a/Source/Teensy/GEOS/GeosDiskPatch.h b/Source/Teensy/GEOS/GeosDiskPatch.h new file mode 100644 index 00000000..13375a03 --- /dev/null +++ b/Source/Teensy/GEOS/GeosDiskPatch.h @@ -0,0 +1,531 @@ +// SPDX-License-Identifier: MIT +// Puts a TR input driver on a GEOS boot disk image, block by block. +// Mirrors tools/geos/trmouse.py install(), which the host test holds it to byte for byte. +#pragma once +#include +#include +#include +#include +#ifdef ARDUINO +#include +#else +#define FLASHMEM +#define PSTR(s) (s) +#endif + +#define GEOS_SECTOR_SIZE 256 +#define GEOS_DIR_ENTRY_SIZE 32 +#define GEOS_ENTRIES_PER_DIR (GEOS_SECTOR_SIZE / GEOS_DIR_ENTRY_SIZE) +#define GEOS_NAME_SIZE 16 +#define GEOS_MAX_DIR_SECTORS 40 +#define GEOS_ERROR_SIZE 80 +#define GEOS_DATA_PER_BLOCK (GEOS_SECTOR_SIZE - 2) + +#define CBM_NAME_PAD 0xa0 +#define CBM_TYPE_MASK 0x07 +#define CBM_USR_CLOSED 0x83 +#define GEOS_SEQUENTIAL 0 +#define D71_DOUBLE_SIDED 0x80 + +// Byte offsets inside a 32-byte directory slot (the first two hold the sector link) +enum GeosSlot +{ + SlotType = 2, + SlotFirst = 3, + SlotName = 5, + SlotInfo = 21, + SlotStructure = 23, + SlotGeosType = 24, + SlotDate = 25, + SlotBlocks = 30, +}; + +struct GeosPlace { uint8_t Track, Sector, Byte; }; +struct GeosTS { uint8_t Track, Sector; }; + +struct GeosFormat +{ + const char *Name; + uint8_t Tracks, DirTrack, FirstDirSector, LastDirSector; + uint8_t Reserved[2]; + uint32_t Size; + bool FixedSectors; // D81: 40 per track; else the 1541's zones, repeated on a 1571's second side + bool TwoSided; +}; + +struct GeosDate { uint8_t Year, Month, Day, Hour, Minute; }; + +// A GEOS version the TR sets up: its boot disk's DeskTop and the input driver the TR adds +struct GeosTarget +{ + const char *DeskTopName, *DriverName; + const uint8_t *DriverCode, *DriverInfo; + uint16_t DriverSize; + uint8_t InputType; +}; + +struct GeosBootDisk { bool HasDeskTop, HasDriver; }; + +// A boot disk image reached through 256-byte block reads and writes at byte offsets. +struct GeosDisk +{ + const struct GeosFormat *Format; + const struct GeosTarget *Target; + void *Context; + bool (*Read)(void *Context, uint32_t Offset, uint8_t *Block); + bool (*Write)(void *Context, uint32_t Offset, const uint8_t *Block); + char Error[GEOS_ERROR_SIZE]; +}; + +static const struct GeosFormat GeosFormats[] = +{ + { "D64", 35, 18, 1, 18, { 18, 0 }, 174848, false, false }, + { "D71", 70, 18, 1, 18, { 18, 53 }, 349696, false, true }, + { "D81", 80, 40, 3, 39, { 40, 0 }, 819200, true, false }, +}; + +FLASHMEM static bool GeosFail(struct GeosDisk *Disk, const char *Message) +{ + snprintf(Disk->Error, sizeof Disk->Error, PSTR("%s"), Message); + return false; +} + +FLASHMEM static uint8_t GeosSectorsOnTrack(const struct GeosFormat *Format, uint8_t Track) +{ + uint8_t ZoneTrack = (Track - 1) % 35 + 1; + + if (Format->FixedSectors) return 40; + return ZoneTrack <= 17 ? 21 : ZoneTrack <= 24 ? 19 : ZoneTrack <= 30 ? 18 : 17; +} + +FLASHMEM static uint32_t GeosOffset(const struct GeosFormat *Format, uint8_t Track, uint8_t Sector) +{ + uint32_t Blocks = Sector; + + for (uint8_t t = 1; t < Track; t++) Blocks += GeosSectorsOnTrack(Format, t); + return Blocks * GEOS_SECTOR_SIZE; +} + +FLASHMEM static bool GeosReadTS(struct GeosDisk *Disk, uint8_t Track, uint8_t Sector, uint8_t *Block) +{ + if (Disk->Read(Disk->Context, GeosOffset(Disk->Format, Track, Sector), Block)) return true; + return GeosFail(Disk, PSTR("the image could not be read")); +} + +FLASHMEM static bool GeosWriteTS(struct GeosDisk *Disk, uint8_t Track, uint8_t Sector, const uint8_t *Block) +{ + if (Disk->Write(Disk->Context, GeosOffset(Disk->Format, Track, Sector), Block)) return true; + return GeosFail(Disk, PSTR("the image could not be written")); +} + +FLASHMEM static bool GeosIsReserved(const struct GeosFormat *Format, uint8_t Track) +{ + return Track == Format->Reserved[0] || Track == Format->Reserved[1]; +} + +FLASHMEM static bool GeosIsDirSector(const struct GeosFormat *Format, uint8_t Track, uint8_t Sector) +{ + return Track == Format->DirTrack && Sector >= Format->FirstDirSector && Sector <= Format->LastDirSector; +} + +// Where a track's free count and free-sector bitmap live +FLASHMEM static void GeosBamPlaces(const struct GeosFormat *Format, uint8_t Track, struct GeosPlace *Count, struct GeosPlace *Bitmap) +{ + if (Format->FixedSectors) + { + uint8_t Sector = 1 + (Track - 1) / 40, Entry = 0x10 + (Track - 1) % 40 * 6; + *Count = (struct GeosPlace){ 40, Sector, Entry }; + *Bitmap = (struct GeosPlace){ 40, Sector, (uint8_t)(Entry + 1) }; + } + else if (Track <= 35) + { + *Count = (struct GeosPlace){ 18, 0, (uint8_t)(4 + (Track - 1) * 4) }; + *Bitmap = (struct GeosPlace){ 18, 0, (uint8_t)(5 + (Track - 1) * 4) }; + } + else + { + *Count = (struct GeosPlace){ 18, 0, (uint8_t)(0xdd + Track - 36) }; + *Bitmap = (struct GeosPlace){ 53, 0, (uint8_t)((Track - 36) * 3) }; + } +} + +FLASHMEM static bool GeosSectorIsFree(const uint8_t *Bitmap, uint8_t Sector) +{ + return Bitmap[Sector / 8] & (1 << (Sector % 8)); +} + +FLASHMEM static uint8_t GeosCountFree(const struct GeosFormat *Format, uint8_t Track, const uint8_t *Bitmap, int *FirstFree) +{ + uint8_t Free = 0; + + *FirstFree = -1; + for (uint8_t s = 0; s < GeosSectorsOnTrack(Format, Track); s++) + { + if (!GeosSectorIsFree(Bitmap, s)) continue; + if (*FirstFree < 0) *FirstFree = s; + Free++; + } + return Free; +} + +// A track's BAM blocks as read; Count and Bits point into them. +struct GeosTrackBam +{ + struct GeosPlace CountAt, BitmapAt; + bool Shared; + uint8_t CountBlock[GEOS_SECTOR_SIZE], BitmapBlock[GEOS_SECTOR_SIZE]; + uint8_t *Count, *Bits, Free; + int FirstFree; +}; + +FLASHMEM static bool GeosReadTrackBam(struct GeosDisk *Disk, uint8_t Track, struct GeosTrackBam *Bam) +{ + GeosBamPlaces(Disk->Format, Track, &Bam->CountAt, &Bam->BitmapAt); + Bam->Shared = Bam->CountAt.Track == Bam->BitmapAt.Track && Bam->CountAt.Sector == Bam->BitmapAt.Sector; + if (!GeosReadTS(Disk, Bam->CountAt.Track, Bam->CountAt.Sector, Bam->CountBlock)) return false; + if (!Bam->Shared && !GeosReadTS(Disk, Bam->BitmapAt.Track, Bam->BitmapAt.Sector, Bam->BitmapBlock)) return false; + Bam->Count = Bam->CountBlock + Bam->CountAt.Byte; + Bam->Bits = (Bam->Shared ? Bam->CountBlock : Bam->BitmapBlock) + Bam->BitmapAt.Byte; + + Bam->Free = GeosCountFree(Disk->Format, Track, Bam->Bits, &Bam->FirstFree); + return true; +} + +FLASHMEM static bool GeosCheckCount(struct GeosDisk *Disk, uint8_t Track, const struct GeosTrackBam *Bam) +{ + if (*Bam->Count >= Bam->Free) return true; + snprintf(Disk->Error, sizeof Disk->Error, PSTR("the BAM count for track %u disagrees with its bitmap"), Track); + return false; +} + +// Marks the first free sector of a track in use; *Sector is -1 when the track has none. +FLASHMEM static bool GeosTake(struct GeosDisk *Disk, uint8_t Track, int *Sector) +{ + struct GeosTrackBam Bam; + + if (!GeosReadTrackBam(Disk, Track, &Bam) || !GeosCheckCount(Disk, Track, &Bam)) return false; + *Sector = Bam.FirstFree; + if (*Sector < 0) return true; + + Bam.Bits[*Sector / 8] &= ~(1 << (*Sector % 8)); + (*Bam.Count)--; + if (!Bam.Shared && !GeosWriteTS(Disk, Bam.BitmapAt.Track, Bam.BitmapAt.Sector, Bam.BitmapBlock)) return false; + return GeosWriteTS(Disk, Bam.CountAt.Track, Bam.CountAt.Sector, Bam.CountBlock); +} + +// Refuses, before anything is written, a disk that GeosAllocate would run out on partway. +// Reads the tracks GeosAllocate would visit, in its order, so the refusals match trmouse.py's. +FLASHMEM static bool GeosCheckRoom(struct GeosDisk *Disk, unsigned Needed) +{ + struct GeosTrackBam Bam; + unsigned Found = 0; + + for (uint8_t Track = 1; Track <= Disk->Format->Tracks; Track++) + { + if (GeosIsReserved(Disk->Format, Track)) continue; + if (!GeosReadTrackBam(Disk, Track, &Bam) || !GeosCheckCount(Disk, Track, &Bam)) return false; + Found += Bam.Free; + if (Found >= Needed) return true; + } + return GeosFail(Disk, PSTR("the disk is full")); +} + +FLASHMEM static bool GeosAllocate(struct GeosDisk *Disk, struct GeosTS *Where) +{ + for (uint8_t Track = 1; Track <= Disk->Format->Tracks; Track++) + { + int Sector; + if (GeosIsReserved(Disk->Format, Track)) continue; + if (!GeosTake(Disk, Track, &Sector)) return false; + if (Sector < 0) continue; + *Where = (struct GeosTS){ Track, (uint8_t)Sector }; + return true; + } + return GeosFail(Disk, PSTR("the disk is full")); +} + +FLASHMEM static bool GeosChainHas(const struct GeosTS *Chain, int Length, uint8_t Track, uint8_t Sector) +{ + for (int i = 0; i < Length; i++) + { + if (Chain[i].Track == Track && Chain[i].Sector == Sector) return true; + } + return false; +} + +FLASHMEM static bool GeosDirChain(struct GeosDisk *Disk, struct GeosTS *Chain, int *Length) +{ + uint8_t Block[GEOS_SECTOR_SIZE]; + uint8_t Track, Sector; + + *Length = 0; + if (!GeosReadTS(Disk, Disk->Format->DirTrack, 0, Block)) return false; + Track = Block[0]; + Sector = Block[1]; + if (!Track) return GeosFail(Disk, PSTR("the disk header links to no directory")); + + while (Track) + { + if (GeosChainHas(Chain, *Length, Track, Sector) || !GeosIsDirSector(Disk->Format, Track, Sector) || + *Length == GEOS_MAX_DIR_SECTORS) + { + snprintf(Disk->Error, sizeof Disk->Error, PSTR("the directory chain is broken at %u/%u"), Track, Sector); + return false; + } + Chain[(*Length)++] = (struct GeosTS){ Track, Sector }; + if (!GeosReadTS(Disk, Track, Sector, Block)) return false; + Track = Block[0]; + Sector = Block[1]; + } + return true; +} + +// Refuses, before anything is written, a directory that GeosGrowDirectory could not grow. +FLASHMEM static bool GeosCheckDirectoryRoom(struct GeosDisk *Disk, const struct GeosTS *Chain, int Length) +{ + const struct GeosFormat *Format = Disk->Format; + struct GeosTrackBam Bam; + + if (!GeosReadTrackBam(Disk, Format->DirTrack, &Bam)) return false; + for (int i = 0; i < Length; i++) + { + if (!GeosSectorIsFree(Bam.Bits, Chain[i].Sector)) continue; + snprintf(Disk->Error, sizeof Disk->Error, PSTR("the BAM shows directory sector %u/%u free"), Chain[i].Track, Chain[i].Sector); + return false; + } + if (!GeosCheckCount(Disk, Format->DirTrack, &Bam)) return false; + if (Bam.FirstFree >= 0 && GeosIsDirSector(Format, Format->DirTrack, Bam.FirstFree)) return true; + return GeosFail(Disk, PSTR("the directory is full")); +} + +// Chains one more directory sector, as DOS does when a directory fills. +FLASHMEM static bool GeosGrowDirectory(struct GeosDisk *Disk, struct GeosTS *Chain, int *Length) +{ + const struct GeosFormat *Format = Disk->Format; + uint8_t Block[GEOS_SECTOR_SIZE]; + int New; + + if (*Length == GEOS_MAX_DIR_SECTORS) return GeosFail(Disk, PSTR("the directory is full")); + if (!GeosCheckDirectoryRoom(Disk, Chain, *Length) || !GeosTake(Disk, Format->DirTrack, &New)) return false; + + struct GeosTS Last = Chain[*Length - 1]; + if (!GeosReadTS(Disk, Last.Track, Last.Sector, Block)) return false; + Block[0] = Format->DirTrack; + Block[1] = New; + if (!GeosWriteTS(Disk, Last.Track, Last.Sector, Block)) return false; + + memset(Block, 0, sizeof Block); + Block[1] = 0xff; + Chain[(*Length)++] = (struct GeosTS){ Format->DirTrack, (uint8_t)New }; + return GeosWriteTS(Disk, Format->DirTrack, New, Block); +} + +FLASHMEM static uint16_t GeosDriverDataBlocks(const struct GeosTarget *Target) +{ + return (Target->DriverSize + GEOS_DATA_PER_BLOCK - 1) / GEOS_DATA_PER_BLOCK; +} + +FLASHMEM static uint16_t GeosDriverBlocks(const struct GeosTarget *Target) +{ + return GeosDriverDataBlocks(Target) + 1; +} + +// Writes the driver's data chain and info block; returns the first data sector, the info sector and the block count. +FLASHMEM static bool GeosWriteDriver(struct GeosDisk *Disk, struct GeosTS *First, struct GeosTS *Info, uint16_t *Blocks) +{ + const struct GeosTarget *Target = Disk->Target; + const uint16_t DataBlocks = GeosDriverDataBlocks(Target); + struct GeosTS Here, Next = { 0, 0 }; + uint8_t Block[GEOS_SECTOR_SIZE]; + + if (!GeosAllocate(Disk, &Here)) return false; + *First = Here; + for (uint16_t i = 0; i < DataBlocks; i++) + { + uint16_t Start = i * GEOS_DATA_PER_BLOCK, Length = Target->DriverSize - Start; + if (Length > GEOS_DATA_PER_BLOCK) Length = GEOS_DATA_PER_BLOCK; + bool Last = i + 1 == DataBlocks; + + if (!Last && !GeosAllocate(Disk, &Next)) return false; + memset(Block, 0, sizeof Block); + Block[0] = Last ? 0 : Next.Track; + Block[1] = Last ? Length + 1 : Next.Sector; + memcpy(Block + 2, Target->DriverCode + Start, Length); + if (!GeosWriteTS(Disk, Here.Track, Here.Sector, Block)) return false; + Here = Next; + } + + if (!GeosAllocate(Disk, Info)) return false; + *Blocks = GeosDriverBlocks(Target); + return GeosWriteTS(Disk, Info->Track, Info->Sector, Target->DriverInfo); +} + +FLASHMEM static void GeosBuildEntry(const struct GeosTarget *Target, uint8_t *Slot, struct GeosTS First, struct GeosTS Info, uint16_t Blocks, const struct GeosDate *Date) +{ + memset(Slot + SlotType, 0, GEOS_DIR_ENTRY_SIZE - SlotType); + Slot[SlotType] = CBM_USR_CLOSED; + Slot[SlotFirst] = First.Track; + Slot[SlotFirst + 1] = First.Sector; + memset(Slot + SlotName, CBM_NAME_PAD, GEOS_NAME_SIZE); + memcpy(Slot + SlotName, Target->DriverName, strlen(Target->DriverName)); + Slot[SlotInfo] = Info.Track; + Slot[SlotInfo + 1] = Info.Sector; + Slot[SlotStructure] = GEOS_SEQUENTIAL; + Slot[SlotGeosType] = Target->InputType; + Slot[SlotDate] = Date->Year % 100; + Slot[SlotDate + 1] = Date->Month; + Slot[SlotDate + 2] = Date->Day; + Slot[SlotDate + 3] = Date->Hour; + Slot[SlotDate + 4] = Date->Minute; + Slot[SlotBlocks] = Blocks & 0xff; + Slot[SlotBlocks + 1] = Blocks >> 8; +} + +FLASHMEM static bool GeosSlotNamed(const uint8_t *Slot, const char *Name) +{ + size_t Length = strlen(Name); + + if (memcmp(Slot + SlotName, Name, Length)) return false; + for (size_t i = Length; i < GEOS_NAME_SIZE; i++) + { + if (Slot[SlotName + i] != CBM_NAME_PAD) return false; + } + return true; +} + +// A slot's place in directory order, as an index over the chain's sectors and their slots +struct GeosSlotRef { int Sector, Slot; }; + +struct GeosDirScan +{ + struct GeosSlotRef Free, FirstInputDriver; + bool HasFree, HasInputDriver, HasDeskTop, HasOurs; +}; + +FLASHMEM static bool GeosScanDirectory(struct GeosDisk *Disk, const struct GeosTS *Chain, int Length, struct GeosDirScan *Scan) +{ + const struct GeosTarget *Target = Disk->Target; + uint8_t Block[GEOS_SECTOR_SIZE]; + + memset(Scan, 0, sizeof *Scan); + for (int i = 0; i < Length; i++) + { + if (!GeosReadTS(Disk, Chain[i].Track, Chain[i].Sector, Block)) return false; + for (int e = 0; e < GEOS_ENTRIES_PER_DIR; e++) + { + const uint8_t *Slot = Block + e * GEOS_DIR_ENTRY_SIZE; + struct GeosSlotRef Here = { i, e }; + bool InUse = Slot[SlotType] & CBM_TYPE_MASK; + + if (!Slot[SlotType] && !Scan->HasFree) { Scan->Free = Here; Scan->HasFree = true; } + if (!InUse) continue; + if (GeosSlotNamed(Slot, Target->DriverName)) Scan->HasOurs = true; + if (GeosSlotNamed(Slot, Target->DeskTopName)) Scan->HasDeskTop = true; + if (Slot[SlotGeosType] == Target->InputType && !Scan->HasInputDriver) { Scan->FirstInputDriver = Here; Scan->HasInputDriver = true; } + } + } + return true; +} + +FLASHMEM static bool GeosSlotBefore(struct GeosSlotRef a, struct GeosSlotRef b) +{ + return a.Sector < b.Sector || (a.Sector == b.Sector && a.Slot < b.Slot); +} + +FLASHMEM static bool GeosSwapEntries(struct GeosDisk *Disk, const struct GeosTS *Chain, struct GeosSlotRef a, struct GeosSlotRef b) +{ + uint8_t BlockA[GEOS_SECTOR_SIZE], BlockB[GEOS_SECTOR_SIZE], Saved[GEOS_DIR_ENTRY_SIZE]; + bool Shared = a.Sector == b.Sector; + uint8_t *SlotA = BlockA + a.Slot * GEOS_DIR_ENTRY_SIZE + SlotType; + uint8_t *SlotB = (Shared ? BlockA : BlockB) + b.Slot * GEOS_DIR_ENTRY_SIZE + SlotType; + const size_t EntryBytes = GEOS_DIR_ENTRY_SIZE - SlotType; + + if (!GeosReadTS(Disk, Chain[a.Sector].Track, Chain[a.Sector].Sector, BlockA)) return false; + if (!Shared && !GeosReadTS(Disk, Chain[b.Sector].Track, Chain[b.Sector].Sector, BlockB)) return false; + memcpy(Saved, SlotA, EntryBytes); + memcpy(SlotA, SlotB, EntryBytes); + memcpy(SlotB, Saved, EntryBytes); + if (!Shared && !GeosWriteTS(Disk, Chain[b.Sector].Track, Chain[b.Sector].Sector, BlockB)) return false; + return GeosWriteTS(Disk, Chain[a.Sector].Track, Chain[a.Sector].Sector, BlockA); +} + +FLASHMEM static bool GeosWriteEntry(struct GeosDisk *Disk, struct GeosTS Where, int Slot, const struct GeosDate *Date) +{ + struct GeosTS First, Info; + uint16_t Blocks; + uint8_t Block[GEOS_SECTOR_SIZE]; + + if (!GeosWriteDriver(Disk, &First, &Info, &Blocks)) return false; + if (!GeosReadTS(Disk, Where.Track, Where.Sector, Block)) return false; + GeosBuildEntry(Disk->Target, Block + Slot * GEOS_DIR_ENTRY_SIZE, First, Info, Blocks, Date); + return GeosWriteTS(Disk, Where.Track, Where.Sector, Block); +} + +FLASHMEM static inline const struct GeosFormat *GeosFormatForSize(uint32_t Size) +{ + for (size_t i = 0; i < sizeof GeosFormats / sizeof GeosFormats[0]; i++) + { + if (GeosFormats[i].Size == Size) return &GeosFormats[i]; + } + return NULL; +} + +FLASHMEM static bool GeosReadDirectory(struct GeosDisk *Disk, struct GeosTS *Chain, int *Length, struct GeosDirScan *Scan) +{ + uint8_t Header[GEOS_SECTOR_SIZE]; + + const struct GeosTarget *Target = Disk->Target; + + if (strlen(Target->DeskTopName) > GEOS_NAME_SIZE || strlen(Target->DriverName) > GEOS_NAME_SIZE || !Target->DriverSize) + { + return GeosFail(Disk, PSTR("the GEOS target's names or driver do not fit a directory entry")); + } + if (!Disk->Format) return GeosFail(Disk, PSTR("not a D64, D71 or D81 without error bytes")); + if (!GeosReadTS(Disk, Disk->Format->DirTrack, 0, Header)) return false; + if (Disk->Format->TwoSided && !(Header[3] & D71_DOUBLE_SIDED)) + { + return GeosFail(Disk, PSTR("this D71 is formatted single-sided")); + } + if (!GeosDirChain(Disk, Chain, Length)) return false; + return GeosScanDirectory(Disk, Chain, *Length, Scan); +} + +// Checks the image and says whether it is the target's boot disk and already holds the TR driver. +FLASHMEM static inline bool GeosInspect(struct GeosDisk *Disk, struct GeosBootDisk *Found) +{ + struct GeosTS Chain[GEOS_MAX_DIR_SECTORS]; + struct GeosDirScan Scan; + int Length; + + if (!GeosReadDirectory(Disk, Chain, &Length, &Scan)) return false; + Found->HasDeskTop = Scan.HasDeskTop; + Found->HasDriver = Scan.HasOurs; + return true; +} + +// Writes the driver ahead of every other input driver of its type: GEOS 128 boots with the first one in the directory. +// Every refusal but a failed block read or write comes before the first write, leaving the image as it was. +FLASHMEM static inline bool GeosInstallDriver(struct GeosDisk *Disk, const struct GeosDate *Date) +{ + struct GeosTS Chain[GEOS_MAX_DIR_SECTORS]; + struct GeosDirScan Scan; + int Length; + + if (!GeosReadDirectory(Disk, Chain, &Length, &Scan)) return false; + if (Scan.HasOurs) + { + snprintf(Disk->Error, sizeof Disk->Error, PSTR("%s is already on this disk"), Disk->Target->DriverName); + return false; + } + if (!Scan.HasFree && !GeosCheckDirectoryRoom(Disk, Chain, Length)) return false; + if (!GeosCheckRoom(Disk, GeosDriverBlocks(Disk->Target))) return false; + if (!Scan.HasFree) + { + if (!GeosGrowDirectory(Disk, Chain, &Length)) return false; + Scan.Free = (struct GeosSlotRef){ Length - 1, 0 }; + } + + if (!GeosWriteEntry(Disk, Chain[Scan.Free.Sector], Scan.Free.Slot, Date)) return false; + if (!Scan.HasInputDriver || !GeosSlotBefore(Scan.FirstInputDriver, Scan.Free)) return true; + return GeosSwapEntries(Disk, Chain, Scan.FirstInputDriver, Scan.Free); +} diff --git a/Source/Teensy/GEOS/GeosTargets.h b/Source/Teensy/GEOS/GeosTargets.h new file mode 100644 index 00000000..144985eb --- /dev/null +++ b/Source/Teensy/GEOS/GeosTargets.h @@ -0,0 +1,19 @@ +// 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 "GeosDiskPatch.h" +#include "TRMouse128.h" + +#define GEOS_INPUT_128 15 + +struct GeosLauncher +{ + const char *Title, *Folder; + struct GeosTarget Target; +}; + +static const struct GeosLauncher Geos128Launcher = +{ + "GEOS 128", "/GEOS128", + { "128 DESKTOP", TRMouse128Name, TRMouse128Code, TRMouse128Info, sizeof TRMouse128Code, GEOS_INPUT_128 }, +}; diff --git a/Source/Teensy/GEOS/TRMouse128.h b/Source/Teensy/GEOS/TRMouse128.h new file mode 100644 index 00000000..52c27ba1 --- /dev/null +++ b/Source/Teensy/GEOS/TRMouse128.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +// Generated by tools/geos/trmouse.py --c-header from tools/geos/trmouse128.asm; do not edit. +#pragma once +#include +#ifdef ARDUINO +#include +#else +#define PROGMEM +#endif + +static const char TRMouse128Name[] PROGMEM = "TR USB MOUSE"; +static const uint8_t TRMouse128Code[207] PROGMEM = { + 0x4c, 0x10, 0xfd, 0x4c, 0x1a, 0xfd, 0x4c, 0x2f, 0xfd, 0x4c, 0x1a, 0xfd, 0x80, 0x00, 0x00, 0x00, + 0xa9, 0x00, 0x85, 0x3b, 0xa9, 0x08, 0x85, 0x3a, 0x85, 0x3c, 0x60, 0xa2, 0x02, 0xbd, 0x03, 0xdf, + 0xdd, 0x2c, 0xfd, 0xd0, 0x05, 0xca, 0x10, 0xf5, 0x18, 0x60, 0x38, 0x60, 0x54, 0x52, 0x4d, 0x20, + 0x1b, 0xfd, 0xb0, 0x10, 0xad, 0x00, 0xdf, 0x8d, 0x0d, 0xfd, 0xad, 0x01, 0xdf, 0x8d, 0x0f, 0xfd, + 0x24, 0x30, 0x30, 0x01, 0x60, 0xad, 0x02, 0xdf, 0x29, 0x01, 0xf0, 0x04, 0xa9, 0x00, 0xf0, 0x02, + 0xa9, 0x80, 0xcd, 0x0c, 0xfd, 0xf0, 0x0c, 0x8d, 0x0c, 0xfd, 0x8d, 0x05, 0x85, 0xa5, 0x39, 0x09, + 0x20, 0x85, 0x39, 0xa2, 0x00, 0xad, 0x0d, 0xfd, 0x10, 0x01, 0xca, 0x8e, 0x0e, 0xfd, 0x24, 0x3f, + 0x10, 0x06, 0x0e, 0x0d, 0xfd, 0x2e, 0x0e, 0xfd, 0x18, 0xa5, 0x3a, 0x6d, 0x0d, 0xfd, 0x85, 0x3a, + 0xa5, 0x3b, 0x6d, 0x0e, 0xfd, 0x85, 0x3b, 0xad, 0x0f, 0xfd, 0x18, 0x65, 0x3c, 0x2c, 0x0f, 0xfd, + 0x30, 0x0a, 0xb0, 0x04, 0xc9, 0xe4, 0x90, 0x08, 0xa9, 0xe3, 0xd0, 0x04, 0xb0, 0x02, 0xa9, 0xff, + 0x85, 0x3c, 0xa2, 0x01, 0xad, 0x0e, 0xfd, 0x30, 0x08, 0x0d, 0x0d, 0xfd, 0xf0, 0x04, 0xe8, 0xd0, + 0x01, 0xca, 0xad, 0x0f, 0xfd, 0xf0, 0x05, 0x30, 0x06, 0xe8, 0xe8, 0xe8, 0xe8, 0xe8, 0xe8, 0xbd, + 0xc6, 0xfd, 0x8d, 0x06, 0x85, 0x60, 0x03, 0x02, 0x01, 0x04, 0xff, 0x00, 0x05, 0x06, 0x07 +}; +static const uint8_t TRMouse128Info[256] PROGMEM = { + 0x00, 0xff, 0x03, 0x15, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x01, 0x91, 0x80, 0x02, 0x10, + 0x40, 0x04, 0x10, 0x20, 0x04, 0x10, 0x20, 0x04, 0x10, 0x20, 0x07, 0xff, 0xe0, 0x04, 0x00, 0x20, + 0x04, 0x00, 0x20, 0x04, 0x00, 0x20, 0x04, 0x00, 0x20, 0x04, 0x00, 0x20, 0x04, 0x00, 0x20, 0x02, + 0x00, 0x40, 0x01, 0x81, 0x80, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x1e, 0xf7, 0x80, 0x04, 0x94, + 0x80, 0x04, 0xe7, 0x80, 0x83, 0x0f, 0x00, 0x00, 0xfd, 0xcf, 0xfd, 0x00, 0x00, 0x54, 0x52, 0x20, + 0x55, 0x53, 0x42, 0x20, 0x4d, 0x6f, 0x75, 0x73, 0x65, 0x56, 0x31, 0x2e, 0x30, 0x00, 0x00, 0x00, + 0x00, 0x54, 0x65, 0x65, 0x6e, 0x73, 0x79, 0x52, 0x4f, 0x4d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x41, 0x20, 0x55, 0x53, 0x42, 0x20, 0x6d, 0x6f, 0x75, 0x73, 0x65, 0x20, 0x70, 0x6c, 0x75, 0x67, + 0x67, 0x65, 0x64, 0x20, 0x69, 0x6e, 0x74, 0x6f, 0x20, 0x61, 0x20, 0x54, 0x65, 0x65, 0x6e, 0x73, + 0x79, 0x52, 0x4f, 0x4d, 0x2e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino new file mode 100644 index 00000000..f9de7ae1 --- /dev/null +++ b/Source/Teensy/GeosLauncher.ino @@ -0,0 +1,224 @@ +// A GEOS menu item: finds the user's boot disk image in the launcher's folder on the SD card and puts +// the TR input driver on a copy of it, leaving the original untouched. + +#include "GEOS/GeosTargets.h" + +#define GEOS_TEMP_NAME "PATCHING.TMP" +#define GEOS_COPY_TAG "-TR" +#define GEOS_EXT_LENGTH 4 +#define GEOS_MAX_IMAGES 8 + +static const char *const GeosImageExtensions[] = { ".d64", ".d71", ".d81" }; + +FLASHMEM static bool GeosFileRead(void *Context, uint32_t Offset, uint8_t *Block) +{ + File *Image = (File *)Context; + return Image->seek(Offset) && Image->read(Block, GEOS_SECTOR_SIZE) == GEOS_SECTOR_SIZE; +} + +FLASHMEM static bool GeosFileWrite(void *Context, uint32_t Offset, const uint8_t *Block) +{ + File *Image = (File *)Context; + return Image->seek(Offset) && Image->write(Block, GEOS_SECTOR_SIZE) == GEOS_SECTOR_SIZE; +} + +FLASHMEM static struct GeosDisk GeosDiskOn(const struct GeosLauncher *Launcher, File *Image) +{ + struct GeosDisk Disk = {}; + uint64_t Size = Image->size(); + + Disk.Format = Size <= UINT32_MAX ? GeosFormatForSize(Size) : NULL; + Disk.Target = &Launcher->Target; + Disk.Context = Image; + Disk.Read = GeosFileRead; + Disk.Write = GeosFileWrite; + return Disk; +} + +FLASHMEM static bool GeosHasImageExtension(const char *Name) +{ + size_t Length = strlen(Name); + + if (Length <= GEOS_EXT_LENGTH) return false; + for (const char *Extension : GeosImageExtensions) + { + if (!strcasecmp(Name + Length - GEOS_EXT_LENGTH, Extension)) return true; + } + return false; +} + +FLASHMEM static bool GeosIsPatchedCopy(const char *Name) +{ + size_t Base = strlen(Name) - GEOS_EXT_LENGTH, TagLength = strlen(GEOS_COPY_TAG); + return Base > TagLength && !strncasecmp(Name + Base - TagLength, GEOS_COPY_TAG, TagLength); +} + +FLASHMEM static void GeosPath(char *Path, size_t Size, const struct GeosLauncher *Launcher, const char *Name) +{ + snprintf(Path, Size, PSTR("%s/%s"), Launcher->Folder, Name); +} + +FLASHMEM static void GeosCopyName(char *CopyName, size_t Size, const char *Name) +{ + int Base = strlen(Name) - GEOS_EXT_LENGTH; + snprintf(CopyName, Size, PSTR("%.*s" GEOS_COPY_TAG "%s"), Base, Name, Name + Base); +} + +// The originals in the folder, listed before anything is written to it +FLASHMEM static uint8_t GeosListImages(const struct GeosLauncher *Launcher, char Names[][MaxItemNameLength]) +{ + File Dir = SD.open(Launcher->Folder); + uint8_t Count = 0; + + if (!Dir) return 0; + for (File Entry = Dir.openNextFile(); Entry && Count < GEOS_MAX_IMAGES; Entry = Dir.openNextFile()) + { + const char *Name = Entry.name(); + if (Entry.isDirectory() || IsDotfile(Name) || !GeosHasImageExtension(Name) || GeosIsPatchedCopy(Name)) continue; + if (strlen(Name) >= MaxItemNameLength) continue; + strcpy(Names[Count++], Name); + } + Dir.close(); + return Count; +} + +FLASHMEM static bool GeosInspectFile(const struct GeosLauncher *Launcher, const char *Path, struct GeosBootDisk *Found, + char *Error, size_t ErrorSize) +{ + File Image = SD.open(Path, FILE_READ); + struct GeosDisk Disk; + bool Inspected; + + if (!Image) + { + if (Error) snprintf(Error, ErrorSize, PSTR("the image could not be opened")); + return false; + } + Disk = GeosDiskOn(Launcher, &Image); + Inspected = GeosInspect(&Disk, Found); + if (!Inspected && Error) snprintf(Error, ErrorSize, PSTR("%s"), Disk.Error); + Image.close(); + return Inspected; +} + +FLASHMEM static bool GeosCopyFile(const char *From, const char *To) +{ + File Source = SD.open(From, FILE_READ), Target; + uint8_t Buffer[GEOS_SECTOR_SIZE * 4]; + uint64_t Total = 0; + bool Copied = true; + int Length; + + SD.remove(To); + Target = SD.open(To, FILE_WRITE); + if (!Source || !Target) Copied = false; + while (Copied && (Length = Source.read(Buffer, sizeof Buffer)) > 0) + { + Copied = Target.write(Buffer, Length) == (size_t)Length; + Total += Length; + } + Copied = Copied && Total == Source.size(); + Source.close(); + Target.close(); + return Copied; +} + +// TimeLib's year() and friends run from boot with no sync provider set, so the RTC is read directly +FLASHMEM static struct GeosDate GeosToday() +{ + int8_t Timezone = (int8_t)IO1[rwRegTimezone]; + tmElements_t Now; + + breakTime(Teensy3Clock.get() + (int16_t)Timezone * 30 * 60, Now); + return (struct GeosDate){ (uint8_t)(tmYearToCalendar(Now.Year) % 100), Now.Month, Now.Day, Now.Hour, Now.Minute }; +} + +FLASHMEM static bool GeosPatchFile(const struct GeosLauncher *Launcher, const char *Path, char *Error, size_t ErrorSize) +{ + File Image = SD.open(Path, FILE_WRITE_BEGIN); + struct GeosDisk Disk; + struct GeosDate Today = GeosToday(); + bool Patched; + + if (!Image) return false; + Disk = GeosDiskOn(Launcher, &Image); + Patched = GeosInstallDriver(&Disk, &Today); + if (!Patched) snprintf(Error, ErrorSize, PSTR("%s"), Disk.Error); + Image.close(); + return Patched; +} + +// Patches a temporary copy and renames it into place, so a failure never leaves a half-patched copy behind +FLASHMEM static bool GeosPatchCopy(const struct GeosLauncher *Launcher, const char *Name, const char *Source, + const char *Copy) +{ + char Temp[MaxNamePathLength], Error[GEOS_ERROR_SIZE]; + + strcpy(Error, PSTR("the SD card could not be written")); + GeosPath(Temp, sizeof Temp, Launcher, GEOS_TEMP_NAME); + if (!GeosCopyFile(Source, Temp) || !GeosPatchFile(Launcher, Temp, Error, sizeof Error) || + (SD.exists(Copy) && !SD.remove(Copy)) || !SD.rename(Temp, Copy)) + { + SD.remove(Temp); + SendMsgPrintfln(PSTR("Could not add the mouse driver to\r\na copy of %s:\r\n%s"), Name, Error); + return false; + } + return true; +} + +FLASHMEM static bool GeosReady(const struct GeosLauncher *Launcher, const char *Name) +{ + SendMsgPrintfln(PSTR("%s boot disk:\r\n%s"), Launcher->Title, Name); + return true; +} + +// Whether the image is a boot disk, set up or not: only with none does the user need telling where to copy one +FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const char *Name) +{ + char Source[MaxNamePathLength], CopyName[MaxItemNameLength + sizeof GEOS_COPY_TAG], Copy[MaxNamePathLength]; + char Error[GEOS_ERROR_SIZE]; + struct GeosBootDisk Found; + + GeosPath(Source, sizeof Source, Launcher, Name); + GeosCopyName(CopyName, sizeof CopyName, Name); + GeosPath(Copy, sizeof Copy, Launcher, CopyName); + if (!GeosInspectFile(Launcher, Source, &Found, Error, sizeof Error)) + { + SendMsgPrintfln(PSTR("%s:\r\n%s"), Name, Error); + return false; + } + if (!Found.HasDeskTop) return false; + if (Found.HasDriver) return GeosReady(Launcher, Name); + + if (SD.exists(Copy)) + { + // The copy is the disk GEOS saves the user's work to: one that cannot be read is left alone, not replaced + if (!GeosInspectFile(Launcher, Copy, &Found, Error, sizeof Error)) + { + SendMsgPrintfln(PSTR("%s:\r\n%s"), CopyName, Error); + return true; + } + if (Found.HasDriver) return GeosReady(Launcher, CopyName); + } + if (!GeosPatchCopy(Launcher, Name, Source, Copy)) return true; + SendMsgPrintfln(PSTR("Added the TR USB mouse driver to\r\na copy of %s"), Name); + return GeosReady(Launcher, CopyName); +} + +FLASHMEM void LaunchGeos(const struct GeosLauncher *Launcher) +{ + char Names[GEOS_MAX_IMAGES][MaxItemNameLength]; + uint8_t Count; + bool BootDiskFound = false; + + if (!SDFullInit()) + { + SendMsgPrintfln(PSTR("%s starts from your boot disk\r\nimage on the SD card. Insert the card."), Launcher->Title); + return; + } + Count = GeosListImages(Launcher, Names); + for (uint8_t i = 0; i < Count; i++) BootDiskFound |= GeosSetUpImage(Launcher, Names[i]); + if (BootDiskFound) return; + SendMsgPrintfln(PSTR("Copy your %s boot disk image\r\n(.D64, .D71 or .D81) to %s\r\non the SD card, then select this again."), + Launcher->Title, Launcher->Folder); +} diff --git a/Source/Teensy/MainMenuItems.h b/Source/Teensy/MainMenuItems.h index e048374e..86c280a2 100644 --- a/Source/Teensy/MainMenuItems.h +++ b/Source/Teensy/MainMenuItems.h @@ -296,6 +296,7 @@ StructMenuItem dirTeensyROMSpecific[] = rtFilePrg , IOH_TR_BASIC , (char*)"BASIC TR Command IO access only" , (uint8_t*)empty_prg , sizeof(empty_prg) , rtBin8kLo , IOH_TeensyROM , (char*)"(This) TeensyROM Menu Cart" , (uint8_t*)TeensyROMC64_bin , sizeof(TeensyROMC64_bin) , rtFilePrg , IOH_TeensyROM , (char*)"TeensyROM External Ports Test" , (uint8_t*)TRExtPortCheck_prg , sizeof(TRExtPortCheck_prg) , + rtGEOS , IOH_None , (char*)"GEOS 128 Boot Disk Setup" , (uint8_t*)&Geos128Launcher , sizeof(Geos128Launcher) , #ifdef Fab04_REU rtFilePrg , IOH_TeensyROM , (char*)"TR+ C64 Expansion Port Test" , (uint8_t*)ExpansionPortTest_prg , sizeof(ExpansionPortTest_prg) , #endif diff --git a/Source/Teensy/MinimalBoot/Common/IOHandlers.h b/Source/Teensy/MinimalBoot/Common/IOHandlers.h index 88bc75b7..0d6da33c 100644 --- a/Source/Teensy/MinimalBoot/Common/IOHandlers.h +++ b/Source/Teensy/MinimalBoot/Common/IOHandlers.h @@ -36,6 +36,7 @@ USBHIDParser hid1(myusbHost); USBHIDParser hid2(myusbHost); USBHIDParser hid3(myusbHost); //need all 3? + MouseController usbHostMouse(myusbHost); USBSerial USBHostSerial(myusbHost); //update HostSerialType to match, defined in PN532_UHSU.h //USBSerial USBHostSerial(myusbHost); // only for those Serial devices who transfer <=64 bytes (like T3.x, FTDI...) @@ -109,6 +110,9 @@ struct stcIOHandlers #ifndef MinimumBuild #include "IO_Handlers/IOH_MIDI.c" #include "IO_Handlers/IOH_Debug.c" + #include "IO_Handlers/RAMBanks.c" + #include "IO_Handlers/USBMouse.c" + #include "IO_Handlers/IOH_GeoRAM.c" #ifdef Fab04_REU #include "IO_Handlers/IOH_REU.c" #endif @@ -158,6 +162,7 @@ stcIOHandlers* IOHandler[] = //Synch order/qty with enum enumIOHandlers &IOHndlr_KernalReplace, //IOH_KernalReplace, #endif &IOHndlr_Debug, //IOH_Debug, + &IOHndlr_GeoRAM, //IOH_GeoRAM, //*not* manually selectable, see LastSelectableIOH diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c new file mode 100644 index 00000000..3f873fb9 --- /dev/null +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT + +// Berkeley Softworks geoRAM: a 256-byte window at $DE00 onto the page selected by two +// write-only registers, $DFFE (page 0-63 within a 16K block) and $DFFF (16K block). +// No DMA, so it runs on every TR, in C64 and C128 mode. +// ref: VICE src/c64/cart/georam.c + +void IO1Hndlr_GeoRAM(uint8_t Address, bool R_Wn); +void IO2Hndlr_GeoRAM(uint8_t Address, bool R_Wn); +void InitHndlr_GeoRAM(); +void PollingHndlr_GeoRAM(); +extern void (*fSpecialBtnChange)(bool Up_nDn); + +stcIOHandlers IOHndlr_GeoRAM = +{ + "GeoRAM", //Name of handler + &InitHndlr_GeoRAM, //Called once at handler startup + &IO1Hndlr_GeoRAM, //IO1 R/W handler + &IO2Hndlr_GeoRAM, //IO2 R/W handler + NULL, //ROML Read handler, in addition to any ROM data sent + NULL, //ROMH Read handler, in addition to any ROM data sent + &PollingHndlr_GeoRAM, //Polled in main routine + NULL, //called at the end of EVERY c64 cycle +}; + +#define GeoRAM_Filename "SD:/georam.ram" +#define MAX_GEORAM_BANKS 64 // 512K, the original geoRAM +#define MIN_GEORAM_BANKS 16 // 128K +#define GeoRAM_PagesPerBlock 64 +#define GeoRAM_BanksPerBlock (16384 / RAM_Bank_Size) + +// VICE decodes the registers across $DF80-$DFFF (A0 picks which); $DF00-$DF7F is the USB mouse's. +#define GeoRAM_RegsBase 0x80 +#define GeoRAM_RegBlockBit 0x01 + +// volatile: the IO ISRs read the page only while there are banks, so the two stores +// must reach memory in the order written +static volatile uint8_t NumGeoRAM_Banks = 0; +static uint8_t *GeoRAM_Bank[MAX_GEORAM_BANKS]; +static uint8_t GeoRAM_Block, GeoRAM_PageNum; +static uint8_t * volatile GeoRAM_Page; + +static void GeoRAM_SelectPage() +{ + if (NumGeoRAM_Banks == 0) return; + // the unused high register bits wrap, as on real hardware + uint32_t Offset = ((GeoRAM_Block & (NumGeoRAM_Banks / GeoRAM_BanksPerBlock - 1)) * GeoRAM_PagesPerBlock + + (GeoRAM_PageNum & (GeoRAM_PagesPerBlock - 1))) * 256; + GeoRAM_Page = GeoRAM_Bank[Offset / RAM_Bank_Size] + (Offset % RAM_Bank_Size); +} + +FLASHMEM void FreeGeoRAM() +{ + // the handler is still installed and the C64 may be running: the IO ISRs must stop + // touching the banks before the first one is freed + uint8_t NumBanks = NumGeoRAM_Banks; + NumGeoRAM_Banks = 0; + FreeRAMBanks(GeoRAM_Bank, &NumBanks); +} + +FLASHMEM void SpecialBtn_GeoRAM(bool Up_nDn) +{ + // on button release save GeoRAM contents + if(Up_nDn) SaveRAMBanksReplacing(GeoRAM_Bank, NumGeoRAM_Banks, GeoRAM_Filename); +} + +FLASHMEM void InitHndlr_GeoRAM() +{ + FreeGeoRAM(); + GeoRAM_Block = GeoRAM_PageNum = 0; + InitUSBMouse(); + uint8_t NumBanks = AllocRAMBanks(GeoRAM_Bank, MAX_GEORAM_BANKS, MIN_GEORAM_BANKS); + if (NumBanks == 0) + { + Serial.printf("GeoRAM is disabled\n"); + return; + } + GeoRAM_Page = GeoRAM_Bank[0]; + NumGeoRAM_Banks = NumBanks; + fSpecialBtnChange = &SpecialBtn_GeoRAM; + Serial.printf("GeoRAM Size: %dK bytes\n", NumBanks * RAM_Bank_Size / 1024); + LoadRAMBanks(GeoRAM_Bank, NumBanks, GeoRAM_Filename); +} + +FLASHMEM void PollingHndlr_GeoRAM() +{ + PollUSBMouse(); +} + +void IO1Hndlr_GeoRAM(uint8_t Address, bool R_Wn) +{ + if (NumGeoRAM_Banks == 0) return; // disabled: $DE00 stays open bus, so software sees no geoRAM + if (R_Wn) DataPortWriteWaitLog(GeoRAM_Page[Address]); + else + { + uint8_t Data = DataPortWaitRead(); + GeoRAM_Page[Address] = Data; + TraceLogAddValidData(Data); + } +} + +void IO2Hndlr_GeoRAM(uint8_t Address, bool R_Wn) +{ + if (Address < GeoRAM_RegsBase) + { + if (R_Wn) USBMouseIORead(Address); + return; + } + if (R_Wn) return; // write-only registers + uint8_t Data = DataPortWaitRead(); + if (Address & GeoRAM_RegBlockBit) GeoRAM_Block = Data; + else GeoRAM_PageNum = Data; + GeoRAM_SelectPage(); + TraceLogAddValidData(Data); +} diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c index 269499dc..36e46597 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c @@ -29,8 +29,7 @@ #ifdef USE_RAM12 - #define REU_RAM_Bank_Size 0x2000 - #define REU_Size (NumREU_Banks * REU_RAM_Bank_Size) //Dynamically allocated based on NumREU_Banks, each bank is 8k (0x2000) + #define REU_Size (NumREU_Banks * RAM_Bank_Size) //Dynamically allocated based on NumREU_Banks, each bank is 8k (0x2000) #define REU_RAM_READ(a,d) d=REU_Bank[a>>13][a&0x1fff] #define REU_RAM_WRITE(a,d) REU_Bank[a>>13][a&0x1fff]=d #define MIN_REU_BANKS 16 // 128K, smallest real REU (1700) @@ -76,11 +75,6 @@ stcIOHandlers IOHndlr_REU = extern bool PerformDMA(DMA_Trans_RnW RnW, uint16_t StartAddr, uint8_t *Buffer, uint32_t Length, DMA_Addr_Mode FixC64Addr); extern bool CloseDMA(); extern void (*fSpecialBtnChange)(bool Up_nDn); //Pointer to function called when Special Button Changes -extern void EEPreadStr(uint16_t addr, char* buf); -extern RegMenuTypes RegMenuTypeFromFileName(char** ptrptrFileName); -extern bool SDFullInit(); -extern bool USBFileSystemWait(); -extern FS *FSfromSourceID(RegMenuTypes SourceID); //ref: https://codebase64.net/doku.php?id=base:reu_registers //tests: @@ -145,17 +139,11 @@ extern bool DMA_RnW, DMA_FixC64Addr; extern uint32_t DMA_Length, DMA_Count, DMA_StartAddr; extern bool DMAByte(uint8_t *Data); -extern void FreeDriveDirMenu(); -extern uint8_t RAM_Image[]; -// CrtChips.h declares the main image's static table and count. extern StructMenuItem *DriveDirMenu; -extern uint8_t *ptrRAM_ImageEnd; // points to end of CRT-used RAM1 buffer void FreeREU() -{ //free banks allocated in RAM2 and reset NumREUBanks - for(uint16_t cnt=0; cnt < NumREU_Banks; cnt++) - if((uint32_t)REU_Bank[cnt] >= 0x20200000) free(REU_Bank[cnt]); - NumREU_Banks = 0; +{ + FreeRAMBanks(REU_Bank, &NumREU_Banks); } void DMAByte_BASkip(uint8_t *Data1) @@ -455,83 +443,12 @@ FLASHMEM void ReadWriteREU(bool RnW, uint32_t REUAddr, uint8_t *REUBuf, uint16_t FLASHMEM void Save_REU() { - if (NumREU_Banks == 0) return; // nothing to save: probably due to failure to allocate 128K - - //Create filename based on current. reu000.reu becomes reu001.reu, etc +#ifndef USE_RAM12 + #error Save_REU saves RAM1/2 banks only +#endif char Filename[MaxPathLength]; EEPreadStr(eepAdREUFilename, Filename); - char *ptrFileName = Filename; //pointer to move past SD/USB/TR: - RegMenuTypes MenuSourceID = RegMenuTypeFromFileName(&ptrFileName); - - if (MenuSourceID == rmtSD) SDFullInit(); // SD.begin(BUILTIN_SDCARD); with retry if presence detected - if (MenuSourceID == rmtUSBDrive) USBFileSystemWait(); //wait up to 1.5 sec in case USB drive just changed or powered up - //rmtTeensy not allowed, no reu files in Teensy Mem - - FS *sourceFS = FSfromSourceID(MenuSourceID); - - char Extension[20]; - char *pDot = strrchr(ptrFileName, '.'); //find last dot - if (pDot == NULL) pDot = ptrFileName + strlen(ptrFileName); //if no extension, set to end of filename - strcpy(Extension, pDot); //copy extension - *pDot = 0; //terminate at dot/extension - - bool IsNum = true; //are last 3digits of filename all numbers? - if (strlen(ptrFileName)>=3) - { - for(char *charloc=pDot-3; charloc'9') IsNum = false; - //Serial.printf("%08x: %c %02x\n", (uint32_t)charloc, *charloc, *charloc); - } - } - else IsNum = false; - - - uint16_t NewNum = 0; - if (IsNum) - { //find current number and then terminate it - NewNum = atoi(pDot-3); - *(pDot-3) = 0; //terminate number - } - - //now see if the file exists, or itterate until it doesn't - char NewFilename[MaxPathLength]; - - do sprintf(NewFilename, "%s%03d%s", ptrFileName, NewNum++, Extension); - while (sourceFS->exists(NewFilename)); - - Serial.printf("Saving REU: %s\n", NewFilename); - File SaveFile = sourceFS->open(NewFilename, FILE_WRITE); - if (!SaveFile) - { - Serial.println("Unable to open!"); - return; - } - - //uint32_t StartmS = millis(); - uint8_t NextByte; - - for (uint32_t CharNum=0; CharNum= 0x20200000) - free(REU_Bank[unused_bank]); - NumREU_Banks = NewNumREU_Banks; - } - - Printf_dbg_reu("After resizing to power of 2, %luK bytes allocated\n", (uint32_t)NumREU_Banks * REU_RAM_Bank_Size / 1024); - - if (NumREU_Banks < MIN_REU_BANKS) // If REU is not 128k or larger + NumREU_Banks = AllocRAMBanks(REU_Bank, MAX_REU_BANKS, MIN_REU_BANKS); + if (NumREU_Banks == 0) { - Serial.printf("Unable to allocate minimum REU size of 128K\n" - "REU is disabled\n"); - FreeREU(); + Serial.printf("REU is disabled\n"); return; } @@ -622,37 +490,9 @@ uint8_t *pRAM_Image = ptrRAM_ImageEnd; //start at end of any CRT-used RAM1 fSpecialBtnChange = &SpecialBtn_REU; //REU RAM is ready; enable button now so a failed/missing preload below doesn't skip it Serial.printf("REU Size: %luK bytes\n", (uint32_t)REU_Size / 1024); - //pre-load REU into RAM here: char Filename[MaxPathLength]; EEPreadStr(eepAdREUFilename, Filename); - char *ptrFileName = Filename; //pointer to move past SD/USB/TR: - Serial.printf("Loading REU: %s\n", Filename); - RegMenuTypes MenuSourceID = RegMenuTypeFromFileName(&ptrFileName); - - if (MenuSourceID == rmtSD) SDFullInit(); // SD.begin(BUILTIN_SDCARD); with retry if presence detected - if (MenuSourceID == rmtUSBDrive) USBFileSystemWait(); //wait up to 1.5 sec in case USB drive just changed or powered up - //rmtTeensy not allowed, no reu files in Teensy Mem - - FS *sourceFS = FSfromSourceID(MenuSourceID); - File LoadFile = sourceFS->open(ptrFileName, FILE_READ); - if (!LoadFile) - { - Serial.println("Not found!"); - return; - } - - if (LoadFile.size() > REU_Size) - Serial.printf("Too Large! Loading first %luK\n", (uint32_t)REU_Size/1024); - - //uint32_t StartmS = millis(); - uint32_t CharNum = 0; - for (uint8_t Bank = 0; Bank < NumREU_Banks && LoadFile.available(); Bank++) - CharNum += LoadFile.read(REU_Bank[Bank], REU_RAM_Bank_Size); - //Serial.printf("Read %lu Bytes in %lumS\n", CharNum, millis()-StartmS); - Serial.printf("Read %lu Bytes into REU\n", CharNum); - LoadFile.close(); - //delay(250); - + LoadRAMBanks(REU_Bank, NumREU_Banks, Filename); #elif defined(USE_PSRAM) //higher clock speed and shorter CS setup/hold do help, but still not enough for NUVIEs w/ sound diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c new file mode 100644 index 00000000..684290d0 --- /dev/null +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c @@ -0,0 +1,237 @@ +// MIT License +// +// Copyright (c) 2026 Travis Smith +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software +// and associated documentation files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +// the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +// RAM expansion memory (REU, GeoRAM): 8k banks taken from the free end of RAM1's RAM_Image, +// then from RAM2, preloaded from and saved to an image file. + +#define RAM_Bank_Size 0x2000 + +extern void EEPreadStr(uint16_t addr, char* buf); +extern RegMenuTypes RegMenuTypeFromFileName(char** ptrptrFileName); +extern bool SDFullInit(); +extern bool USBFileSystemWait(); +extern FS *FSfromSourceID(RegMenuTypes SourceID); +extern void FreeDriveDirMenu(); +extern uint8_t RAM_Image[]; +extern uint8_t *ptrRAM_ImageEnd; // points to end of CRT-used RAM1 buffer + +FLASHMEM void FreeRAMBanks(uint8_t *Bank[], uint8_t *NumBanks) +{ + for(uint16_t cnt=0; cnt < *NumBanks; cnt++) + if((uint32_t)Bank[cnt] >= 0x20200000) free(Bank[cnt]); + *NumBanks = 0; +} + +// Returns the number of banks allocated: a power of 2 up to MaxBanks, or 0 if fewer than MinBanks fit. +FLASHMEM uint8_t AllocRAMBanks(uint8_t *Bank[], uint8_t MaxBanks, uint8_t MinBanks) +{ + uint8_t NumBanks = 0; + FreeDriveDirMenu(); + + if (NumCrtChips == 0) ptrRAM_ImageEnd = RAM_Image; //If no CRT loaded, RAM1 is entirely free + uint8_t *pRAM_Image = ptrRAM_ImageEnd; //start at end of any CRT-used RAM1 + + while (((uint32_t)pRAM_Image - (uint32_t)RAM_Image + RAM_Bank_Size <= RAM_ImageSize) + && (NumBanks < MaxBanks)) + { + Bank[NumBanks] = pRAM_Image; + pRAM_Image += RAM_Bank_Size; + NumBanks++; + } + + while(NumBanks < MaxBanks && (NULL != (Bank[NumBanks] = (uint8_t*)malloc(RAM_Bank_Size)))) + NumBanks++; + + if (NumBanks & (NumBanks-1)) //not a power of 2 + { + // largest power less than or equal to NumBanks + uint8_t NewNumBanks = 1 << (7 - (__builtin_clz(NumBanks) - 24)); + Serial.printf("Resizing from %d banks to %d banks\n", NumBanks, NewNumBanks); + + for (uint8_t unused_bank = NewNumBanks; unused_bank < NumBanks; unused_bank++) + if((uint32_t)Bank[unused_bank] >= 0x20200000) + free(Bank[unused_bank]); + NumBanks = NewNumBanks; + } + + if (NumBanks < MinBanks) + { + Serial.printf("Unable to allocate minimum size of %dK\n", MinBanks * RAM_Bank_Size / 1024); + FreeRAMBanks(Bank, &NumBanks); + } + return NumBanks; +} + +// Path is source-prefixed ("SD:/reu.reu") +FLASHMEM static FS *RAMBanksFS(char *Path, char **ptrFileName) +{ + *ptrFileName = Path; + RegMenuTypes MenuSourceID = RegMenuTypeFromFileName(ptrFileName); + + if (MenuSourceID == rmtSD) SDFullInit(); // SD.begin(BUILTIN_SDCARD); with retry if presence detected + if (MenuSourceID == rmtUSBDrive) USBFileSystemWait(); //wait up to 1.5 sec in case USB drive just changed or powered up + //rmtTeensy not allowed, no RAM image files in Teensy Mem + + return FSfromSourceID(MenuSourceID); +} + +FLASHMEM void LoadRAMBanks(uint8_t *Bank[], uint8_t NumBanks, const char *SourcePath) +{ + char Filename[MaxPathLength]; + strlcpy(Filename, SourcePath, MaxPathLength); + Serial.printf("Loading: %s\n", Filename); + char *ptrFileName; + FS *sourceFS = RAMBanksFS(Filename, &ptrFileName); + File LoadFile = sourceFS->open(ptrFileName, FILE_READ); + if (!LoadFile) + { + Serial.println("Not found!"); + return; + } + + if (LoadFile.size() > (uint32_t)NumBanks * RAM_Bank_Size) + Serial.printf("Too Large! Loading first %luK\n", (uint32_t)NumBanks * RAM_Bank_Size / 1024); + + uint32_t CharNum = 0; + for (uint8_t BankNum = 0; BankNum < NumBanks && LoadFile.available(); BankNum++) + { + int BytesRead = LoadFile.read(Bank[BankNum], RAM_Bank_Size); + if (BytesRead <= 0) break; + CharNum += BytesRead; + } + Serial.printf("Read %lu Bytes\n", CharNum); + LoadFile.close(); +} + +FLASHMEM static bool WriteRAMBanksFile(FS *sourceFS, const char *FileName, uint8_t *Bank[], uint8_t NumBanks) +{ + Serial.printf("Saving: %s\n", FileName); + sourceFS->remove(FileName); // FILE_WRITE appends to an existing file + File SaveFile = sourceFS->open(FileName, FILE_WRITE); + if (!SaveFile) + { + Serial.println("Unable to open!"); + return false; + } + + for (uint8_t BankNum = 0; BankNum < NumBanks; BankNum++) + { + if (SaveFile.write(Bank[BankNum], RAM_Bank_Size) != RAM_Bank_Size) + { + Serial.printf("Write failed after %dKBytes!\n", BankNum * RAM_Bank_Size / 1024); + SaveFile.close(); + return false; + } + } + Serial.printf("Saved %dKBytes\n", NumBanks * RAM_Bank_Size / 1024); + SaveFile.close(); + return true; +} + +FLASHMEM static void FlashSavedLED() +{ + uint32_t LastmS = millis(); + bool LEDState = true; + + for (uint32_t FlashCount=0; FlashCount<20; FlashCount++) + { + while(millis()-LastmS < 75); + LastmS = millis(); + if ((LEDState = !LEDState)) SetLEDOn; + else SetLEDOff; + } +} + +// Saves to the first free numbered name: reu.reu becomes reu000.reu, reu000.reu becomes reu001.reu, etc +FLASHMEM void SaveRAMBanks(uint8_t *Bank[], uint8_t NumBanks, const char *SourcePath) +{ + if (NumBanks == 0) return; // nothing to save: probably due to failure to allocate + + char Filename[MaxPathLength]; + strlcpy(Filename, SourcePath, MaxPathLength); + char *ptrFileName; + FS *sourceFS = RAMBanksFS(Filename, &ptrFileName); + + char Extension[20]; + char *pDot = strrchr(ptrFileName, '.'); + if (pDot == NULL) pDot = ptrFileName + strlen(ptrFileName); //if no extension, set to end of filename + strlcpy(Extension, pDot, sizeof(Extension)); + *pDot = 0; + + bool IsNum = true; //are last 3digits of filename all numbers? + if (strlen(ptrFileName)>=3) + { + for(char *charloc=pDot-3; charloc'9') IsNum = false; + } + else IsNum = false; + + uint16_t NewNum = 0; + if (IsNum) + { //find current number and then terminate it + NewNum = atoi(pDot-3); + *(pDot-3) = 0; //terminate number + } + + char NewFilename[MaxPathLength]; + + do sprintf(NewFilename, "%s%03d%s", ptrFileName, NewNum++, Extension); + while (sourceFS->exists(NewFilename)); + + if (WriteRAMBanksFile(sourceFS, NewFilename, Bank, NumBanks)) FlashSavedLED(); +} + +FLASHMEM static void WithExtension(char *Dest, const char *FileName, const char *Extension) +{ + strlcpy(Dest, FileName, MaxPathLength); + char *pDot = strrchr(Dest, '.'); + if (pDot != NULL) *pDot = 0; + strlcat(Dest, Extension, MaxPathLength); +} + +// Overwrites the file, keeping the one it replaces as .bak: georam.ram becomes georam.bak +FLASHMEM void SaveRAMBanksReplacing(uint8_t *Bank[], uint8_t NumBanks, const char *SourcePath) +{ + if (NumBanks == 0) return; // nothing to save: probably due to failure to allocate + + char Filename[MaxPathLength]; + strlcpy(Filename, SourcePath, MaxPathLength); + char *ptrFileName; + FS *sourceFS = RAMBanksFS(Filename, &ptrFileName); + + char TempFilename[MaxPathLength], BackupFilename[MaxPathLength]; + WithExtension(TempFilename, ptrFileName, ".tmp"); + WithExtension(BackupFilename, ptrFileName, ".bak"); + + if (!WriteRAMBanksFile(sourceFS, TempFilename, Bank, NumBanks)) return; + bool HadSave = sourceFS->exists(ptrFileName); + if (HadSave) + { // rename refuses an existing target + sourceFS->remove(BackupFilename); + sourceFS->rename(ptrFileName, BackupFilename); + } + if (!sourceFS->rename(TempFilename, ptrFileName)) + { + Serial.printf("Unable to rename %s to %s!\n", TempFilename, ptrFileName); + if (HadSave) sourceFS->rename(BackupFilename, ptrFileName); + return; + } + FlashSavedLED(); +} diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c new file mode 100644 index 00000000..c3612801 --- /dev/null +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT + +// A USB mouse on the TR's USB host port, read through registers the GeoRAM handler serves +// at $DF00, in the part of the IO2 page that GeoRAM leaves free. +// It cannot be a 1351: the SID's POT lines are not on the cartridge port, so only software +// with a TR driver sees it (tools/geos). +// +// +0 X movement since the last read of +0, signed, right is positive +// +1 Y movement since the last read of +1, signed, down is positive +// +2 buttons, 1 = pressed: bit 0 left, bit 1 right, bit 2 middle +// +3..5 "TRM", for a driver to tell that the registers are there + +#define USBMouse_RegButtons 2 +#define USBMouse_RegID 3 +#define USBMouse_StepShift 2 // USB mice count ~1000 per inch; GEOS's screen is 320 wide +#define USBMouse_MaxStep 127 +#define USBMouse_ReadyByte 0xff +#define USBMouse_ReadyNext 0x100 + +static const char USBMouse_ID[] = "TRM"; +static volatile int32_t USBMouse_Counts[2]; // USB counts, written only by the main loop +static volatile int32_t USBMouse_StepsRead[2]; // written only by the IO ISR, after InitUSBMouse +static volatile uint8_t USBMouse_Buttons; +// The next read's byte, kept ready so a read drives the bus before any arithmetic. The bits +// above it count reads, so the main loop can tell that a read slipped in while it computed. +static volatile uint32_t USBMouse_Ready[2]; + +static int32_t USBMouse_Steps(uint8_t Axis) +{ + return USBMouse_Counts[Axis] >> USBMouse_StepShift; // floors, so no wider dead zone at 0 +} + +static uint8_t USBMouse_NextStep(uint8_t Axis) +{ + int32_t Step = USBMouse_Steps(Axis) - USBMouse_StepsRead[Axis]; + if (Step > USBMouse_MaxStep) Step = USBMouse_MaxStep; + else if (Step < -USBMouse_MaxStep) Step = -USBMouse_MaxStep; + return (uint8_t)Step; +} + +static void USBMouse_UpdateReady() +{ + for (uint8_t Axis = 0; Axis < 2; Axis++) + { + uint32_t Ready = __atomic_load_n(&USBMouse_Ready[Axis], __ATOMIC_RELAXED); + uint32_t Next; + do Next = (Ready & ~USBMouse_ReadyByte) | USBMouse_NextStep(Axis); + while (!__atomic_compare_exchange_n(&USBMouse_Ready[Axis], &Ready, Next, true, __ATOMIC_RELAXED, __ATOMIC_RELAXED)); + } +} + +FLASHMEM void PollUSBMouse() +{ + // MouseController is also a BTHIDInput, so its own bool conversion is ambiguous + if (!static_cast(usbHostMouse)) + { + // unplugging leaves the library's last report in place, for a replug to deliver + usbHostMouse.mouseDataClear(); + USBMouse_Buttons = 0; + return; + } + if (!usbHostMouse.available()) return; + // the USB ISR writes each report straight into the controller, and mouseDataClear() + // would drop one that lands between these reads and the clear + NVIC_DISABLE_IRQ(IRQ_USBHS); + // the Cortex-M7 can still take the IRQ after the NVIC write until a DSB and ISB + asm volatile("dsb\n\tisb" ::: "memory"); + int32_t X = usbHostMouse.getMouseX(); + int32_t Y = usbHostMouse.getMouseY(); + uint8_t Buttons = usbHostMouse.getButtons(); + usbHostMouse.mouseDataClear(); + asm volatile("" ::: "memory"); + NVIC_ENABLE_IRQ(IRQ_USBHS); + USBMouse_Counts[0] += X; + USBMouse_Counts[1] += Y; + USBMouse_Buttons = Buttons; + USBMouse_UpdateReady(); +} + +// Movement from before the handler started (at the TR menu) is not the C64's to see +FLASHMEM void InitUSBMouse() +{ + usbHostMouse.mouseDataClear(); + for (uint8_t Axis = 0; Axis < 2; Axis++) + { + USBMouse_StepsRead[Axis] = USBMouse_Steps(Axis); + USBMouse_Ready[Axis] = 0; + } +} + +void USBMouseIORead(uint8_t Address) +{ + if (Address < 2) + { + uint32_t Ready = USBMouse_Ready[Address]; + DataPortWriteWaitLog((uint8_t)Ready); + USBMouse_StepsRead[Address] += (int8_t)Ready; + USBMouse_Ready[Address] = (Ready & ~USBMouse_ReadyByte) + USBMouse_ReadyNext + USBMouse_NextStep(Address); + } + else if (Address == USBMouse_RegButtons) DataPortWriteWaitLog(USBMouse_Buttons); + else if (Address < USBMouse_RegID + sizeof(USBMouse_ID) - 1) + DataPortWriteWaitLog(USBMouse_ID[Address - USBMouse_RegID]); +} diff --git a/Source/Teensy/MinimalBoot/Common/Menu_Regs.h b/Source/Teensy/MinimalBoot/Common/Menu_Regs.h index 166ecfe0..9798d633 100644 --- a/Source/Teensy/MinimalBoot/Common/Menu_Regs.h +++ b/Source/Teensy/MinimalBoot/Common/Menu_Regs.h @@ -403,6 +403,7 @@ enum regItemTypes //synch with TblItemType rtBinC128 = 18, rtFileREU = 19, rtFileTRH = 20, + rtGEOS = 21, //63 max: rRegItemTypePlusIOH uses bit 7 (assigned IOH) and bit 6 (/VMS claim), //and TblItemType's mult by 4 limits it to 63 as well @@ -437,7 +438,8 @@ enum enumIOHandlers //Synch order/qty with IOHandler[] (IOHandlers.h) #ifdef Fab04_KernalReplace IOH_KernalReplace, #endif - IOH_Debug, //last manually selectable, see LastSelectableIOH + IOH_Debug, + IOH_GeoRAM, //last manually selectable, see LastSelectableIOH //*not* manually selectable, see LastSelectableIOH @@ -471,4 +473,4 @@ enum enumIOHandlers //Synch order/qty with IOHandler[] (IOHandlers.h) IOH_Num_Handlers //always last }; -#define LastSelectableIOH IOH_Debug //127 max. Used in IOH_TeensyROM.c for Special IO display/select +#define LastSelectableIOH IOH_GeoRAM //127 max. Used in IOH_TeensyROM.c for Special IO display/select diff --git a/Source/Teensy/RemoteControl.ino b/Source/Teensy/RemoteControl.ino index cfa10e5e..351577da 100644 --- a/Source/Teensy/RemoteControl.ino +++ b/Source/Teensy/RemoteControl.ino @@ -273,6 +273,7 @@ void RemoteLaunch(RegMenuTypes MenuSourceID, const char *FileNamePath, bool DoCa #ifdef Fab04_REU FreeREU(); #endif + FreeGeoRAM(); FreeSwiftlinkBuffs(); if (MenuSourceID == rmtTeensy) diff --git a/Source/Teensy/TRMenuFiles/ROMs/TeensyROMC64.h b/Source/Teensy/TRMenuFiles/ROMs/TeensyROMC64.h index e48e852f..56db8d2f 100644 --- a/Source/Teensy/TRMenuFiles/ROMs/TeensyROMC64.h +++ b/Source/Teensy/TRMenuFiles/ROMs/TeensyROMC64.h @@ -389,7 +389,8 @@ static const unsigned char TeensyROMC64_bin[] = { 0x30, 0x00, 0xd3, 0xc9, 0xc4, 0x00, 0xcb, 0x4c, 0x41, 0x00, 0xc1, 0x52, 0x54, 0x00, 0xd4, 0x58, 0x54, 0x00, 0xd3, 0x45, 0x51, 0x00, 0x31, 0x36, 0x4b, 0x00, 0x38, 0xc8, 0x49, 0x00, 0x38, 0xcc, 0x4f, 0x00, 0x31, 0x32, - 0x38, 0x00, 0xd2, 0xc5, 0xd5, 0x00, 0xd4, 0xd2, 0xc8, 0x00, 0x00 + 0x38, 0x00, 0xd2, 0xc5, 0xd5, 0x00, 0xd4, 0xd2, 0xc8, 0x00, 0xc7, 0xc5, + 0xcf, 0x00, 0x00 }; #endif /* TEENSYROMC64_BIN_H */ diff --git a/Source/Teensy/Teensy.ino b/Source/Teensy/Teensy.ino index ff171ca6..e4b40ee5 100644 --- a/Source/Teensy/Teensy.ino +++ b/Source/Teensy/Teensy.ino @@ -36,6 +36,7 @@ #include "MinimalBoot/Common/DMAControl.c" #include "MinimalBoot/Common/DriveDirLoad.h" #include "LoadedListing.h" +#include "GEOS/GeosTargets.h" #include "MainMenuItems.h" #ifdef VM_EXTENSIONS_ENABLED // Before IOHandlers.h: PollingHndlr_TeensyROM is where the record is shown. @@ -420,7 +421,8 @@ FLASHMEM void SetUpMainMenuROM() FreeCrtChips(); #ifdef Fab04_REU FreeREU(); -#endif +#endif + FreeGeoRAM(); FreeSwiftlinkBuffs(); RedirectEmptyDriveDirMenu(); free((void*)MIDIRxBuf); MIDIRxBuf=NULL; diff --git a/Source/Teensy/tests/c64-message-buffer.test.js b/Source/Teensy/tests/c64-message-buffer.test.js index 65b0767e..c86bbccc 100644 --- a/Source/Teensy/tests/c64-message-buffer.test.js +++ b/Source/Teensy/tests/c64-message-buffer.test.js @@ -9,7 +9,7 @@ const FIRMWARE_ROOT = path.resolve(import.meta.dirname, '..'); const SOURCE_EXTENSIONS = new Set(['.ino', '.c', '.cpp', '.h']); const SKIPPED_DIRECTORIES = new Set(['tests', 'TRMenuFiles']); -const MESSAGE_FORMATTERS = /\b(SendMsgPrintfln|SendMsgPrintf|SendStrPrintfln)\s*\(\s*(\S)/g; +const MESSAGE_FORMATTERS = /\b(SendMsgPrintfln|SendMsgPrintf|SendStrPrintfln)\s*\(\s*(?:PSTR\s*\(\s*)?(\S)/g; const FORMATTER_DECLARATION = /^\s*(?:FLASHMEM\s+|extern\s+)*(?:void|bool)\s+Send(?:Msg|Str)/; function firmwareSources(dir = FIRMWARE_ROOT) { @@ -45,6 +45,9 @@ test('the scan still sees a call wrapped after its open paren', () => { ['x.ino:1 SendMsgPrintfln']); assert.deepEqual(nonLiteralFormats('SendMsgPrintfln(\n "%s", MenuSelCpy.Name);\n', 'x.ino'), []); assert.deepEqual(nonLiteralFormats('/* SendMsgPrintfln(Name); */\n', 'x.ino'), []); + assert.deepEqual(nonLiteralFormats('SendMsgPrintfln(PSTR("%s"), Name);\n', 'x.ino'), []); + assert.deepEqual(nonLiteralFormats('SendMsgPrintfln(PSTR(Name));\n', 'x.ino'), + ['x.ino:1 SendMsgPrintfln']); }); test('no caller throws away whether the current path fit', () => { diff --git a/Source/Teensy/tests/geos-disk-patch.test.js b/Source/Teensy/tests/geos-disk-patch.test.js new file mode 100644 index 00000000..e2a90ab5 --- /dev/null +++ b/Source/Teensy/tests/geos-disk-patch.test.js @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: MIT +// The firmware's GEOS boot-disk patch, held byte for byte to tools/geos/trmouse.py on synthetic boot disks. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; + +import { commandExists } from '../../../tools/lib/toolchain.mjs'; + +const FIRMWARE_ROOT = path.resolve(import.meta.dirname, '..'); +const TRMOUSE = path.resolve(FIRMWARE_ROOT, '../../tools/geos/trmouse.py'); +const HARNESS_SOURCE = path.join(FIRMWARE_ROOT, 'tests/geos_disk_patch_harness.c'); + +const NOT_CI = ['', '0', 'false']; +const inCi = !NOT_CI.includes(process.env.CI ?? ''); +const missingTool = ['acme', 'python3', 'cc', 'c++'].find((tool) => !commandExists(tool)); +const skip = missingTool && !inCi && `${missingTool} is not on PATH`; + +const SECTOR = 256; +const PAD = 0xa0; +const USR_CLOSED = 0x83; +const INPUT_128 = 15; +const MOUSE_NAME = 'TR USB MOUSE'; +const DATE_IN_SLOT = 25; +const DATE_BYTES = 5; +const REFUSED_AFTER_WRITING = 3; + +const zoneSectors = (track) => { + const zone = (track - 1) % 35 + 1; + return zone <= 17 ? 21 : zone <= 24 ? 19 : zone <= 30 ? 18 : 17; +}; + +const FORMATS = { + d64: { tracks: 35, sectors: zoneSectors, dirTrack: 18, firstDir: 1 }, + d71: { tracks: 70, sectors: zoneSectors, dirTrack: 18, firstDir: 1 }, + d81: { tracks: 80, sectors: () => 40, dirTrack: 40, firstDir: 3 }, +}; + +function offset(format, track, sector) { + let blocks = sector; + for (let t = 1; t < track; t++) blocks += format.sectors(t); + return blocks * SECTOR; +} + +function imageSize(format) { + return offset(format, format.tracks + 1, 0); +} + +function bamPlaces(kind, track) { + if (kind === 'd81') { + const entry = 0x10 + ((track - 1) % 40) * 6; + const sector = 1 + Math.floor((track - 1) / 40); + return { count: [40, sector, entry], bitmap: [40, sector, entry + 1] }; + } + if (track <= 35) return { count: [18, 0, 4 + (track - 1) * 4], bitmap: [18, 0, 5 + (track - 1) * 4] }; + return { count: [18, 0, 0xdd + track - 36], bitmap: [53, 0, (track - 36) * 3] }; +} + +function at(disk, [track, sector, byte]) { + return offset(disk.format, track, sector) + byte; +} + +function setTrackFree(disk, track, freeSectors) { + const { count, bitmap } = bamPlaces(disk.kind, track); + const bits = at(disk, bitmap); + for (let s = 0; s < disk.format.sectors(track); s++) { + const mask = 1 << (s % 8); + if (freeSectors.includes(s)) disk.image[bits + (s >> 3)] |= mask; + else disk.image[bits + (s >> 3)] &= ~mask; + } + disk.image[at(disk, count)] = freeSectors.length; +} + +function allSectors(disk, track) { + return Array.from({ length: disk.format.sectors(track) }, (_, s) => s); +} + +function writeHeader(disk) { + const { image, format, kind } = disk; + const header = offset(format, format.dirTrack, 0); + image.set([format.dirTrack, format.firstDir], header); + if (kind === 'd81') { + image.set([40, 2, 0x44, 0xbb], offset(format, 40, 1)); + image.set([0, 0xff, 0x44, 0xbb], offset(format, 40, 2)); + } + if (kind === 'd71') image[header + 3] = 0x80; + image[offset(format, format.dirTrack, format.firstDir) + 1] = 0xff; +} + +function blankDisk(kind) { + const format = FORMATS[kind]; + const disk = { kind, format, image: new Uint8Array(imageSize(format)) }; + writeHeader(disk); + for (let track = 1; track <= format.tracks; track++) setTrackFree(disk, track, allSectors(disk, track)); + const systemSectors = kind === 'd81' ? [0, 1, 2, 3] : [0, 1]; + setTrackFree(disk, format.dirTrack, allSectors(disk, format.dirTrack).filter((s) => !systemSectors.includes(s))); + if (kind === 'd71') setTrackFree(disk, 53, []); + return disk; +} + +function putEntry(disk, slot, { name, type = USR_CLOSED, geosType = 0 }) { + const base = offset(disk.format, disk.format.dirTrack, disk.format.firstDir) + slot * 32; + disk.image.fill(PAD, base + 5, base + 21); + disk.image.set(Buffer.from(name, 'latin1'), base + 5); + disk.image[base + 2] = type; + disk.image[base + 24] = geosType; +} + +function withDriver(kind) { + const disk = blankDisk(kind); + putEntry(disk, 0, { name: '128 JOYSTICK', geosType: INPUT_128 }); + putEntry(disk, 1, { name: '128 DESKTOP', geosType: 6 }); + return disk; +} + +function fullDirectory(kind) { + const disk = blankDisk(kind); + for (let slot = 0; slot < 8; slot++) { + putEntry(disk, slot, slot === 3 ? { name: '128 JOYSTICK', geosType: INPUT_128 } : { name: `FILE ${slot}` }); + } + return disk; +} + +function withOneDataSector(disk) { + const reserved = disk.kind === 'd71' ? [18, 53] : [disk.format.dirTrack]; + for (let track = 1; track <= disk.format.tracks; track++) { + if (!reserved.includes(track)) setTrackFree(disk, track, track === 1 ? [0] : []); + } + return disk; +} + +const PATCHED_CASES = { + 'a blank boot disk': blankDisk, + 'a disk whose first input driver comes earlier': withDriver, + 'a full first directory sector': fullDirectory, +}; + +const REFUSED_CASES = { + 'a disk that already has the driver': (kind) => { + const disk = withDriver(kind); + putEntry(disk, 2, { name: MOUSE_NAME, geosType: INPUT_128 }); + return disk; + }, + 'a BAM count below its bitmap': (kind) => { + const disk = blankDisk(kind); + disk.image[at(disk, bamPlaces(kind, 1).count)] -= 1; + return disk; + }, + 'a header linking outside the directory': (kind) => { + const disk = blankDisk(kind); + disk.image[offset(disk.format, disk.format.dirTrack, 0)] = 1; + return disk; + }, + 'a disk with room for the data block but not the info block': (kind) => withOneDataSector(blankDisk(kind)), + 'a full directory on a disk with room for the data block but not the info block': (kind) => + withOneDataSector(fullDirectory(kind)), + 'a full directory whose track has only a system sector free': (kind) => { + const disk = fullDirectory(kind); + setTrackFree(disk, disk.format.dirTrack, [0]); + return disk; + }, + 'a full directory whose BAM frees its sector': (kind) => { + const disk = fullDirectory(kind); + const free = allSectors(disk, disk.format.dirTrack).filter((s) => s >= disk.format.firstDir); + setTrackFree(disk, disk.format.dirTrack, free); + return disk; + }, +}; + +function maskMouseDate(image) { + const name = Buffer.concat([Buffer.from(MOUSE_NAME, 'latin1'), Buffer.alloc(16 - MOUSE_NAME.length, PAD)]); + const found = Buffer.from(image).indexOf(name); + assert.notEqual(found, -1, 'the patched image has no TR USB MOUSE entry'); + const date = found - 5 + DATE_IN_SLOT; + const masked = Buffer.from(image); + masked.fill(0, date, date + DATE_BYTES); + return masked; +} + +function run(command, args) { + return spawnSync(command, args, { encoding: 'utf8' }); +} + +function patchBoth(dir, label, image) { + const input = path.join(dir, `${label}.in`); + const viaPython = path.join(dir, `${label}.py.out`); + const viaFirmware = path.join(dir, `${label}.c.out`); + fs.writeFileSync(input, image); + return { + python: { ...run('python3', [TRMOUSE, input, viaPython]), out: viaPython }, + firmware: { ...run(path.join(dir, 'harness'), [input, viaFirmware]), out: viaFirmware }, + }; +} + +test('the firmware patch matches trmouse.py', { skip }, async (t) => { + assert.equal(missingTool, undefined, 'CI installs every tool this test needs, so it must not skip there'); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'geos-disk-patch-')); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + + const built = run('cc', ['-std=c99', '-Wall', '-Wextra', '-Werror', '-o', path.join(dir, 'harness'), HARNESS_SOURCE]); + assert.equal(built.status, 0, built.stderr); + const asCpp = run('c++', ['-x', 'c++', '-std=gnu++17', '-Wall', '-Werror', '-fsyntax-only', HARNESS_SOURCE]); + assert.equal(asCpp.status, 0, `the firmware compiles this as C++: ${asCpp.stderr}`); + + for (const kind of Object.keys(FORMATS)) { + for (const [label, make] of Object.entries(PATCHED_CASES)) { + await t.test(`${kind}: ${label}`, () => { + const { python, firmware } = patchBoth(dir, `${kind}-${label}`, make(kind).image); + assert.equal(python.status, 0, python.stderr); + assert.equal(firmware.status, 0, firmware.stderr); + assert.deepEqual(maskMouseDate(fs.readFileSync(firmware.out)), maskMouseDate(fs.readFileSync(python.out))); + }); + } + for (const [label, make] of Object.entries(REFUSED_CASES)) { + await t.test(`${kind}: refuses ${label}`, () => { + const { python, firmware } = patchBoth(dir, `${kind}-${label}`, make(kind).image); + assert.notEqual(firmware.status, REFUSED_AFTER_WRITING, `the refusal changed the image: ${firmware.stderr}`); + assert.equal(firmware.status, 1, `the firmware patched it: ${firmware.stderr}`); + assert.notEqual(python.status, 0, 'trmouse.py patched it'); + assert.equal(firmware.stderr.trim(), python.stderr.trim()); + }); + } + } + + for (const kind of Object.keys(FORMATS)) { + await t.test(`${kind}: inspect tells a boot disk from a data disk, and sees the driver once added`, () => { + const inspect = (label, image) => { + const file = path.join(dir, `${kind}-inspect-${label}`); + fs.writeFileSync(file, image); + const result = run(path.join(dir, 'harness'), ['--inspect', file]); + assert.equal(result.status, 0, result.stderr); + return result.stdout.trim(); + }; + const bootDisk = withDriver(kind); + const { firmware } = patchBoth(dir, `${kind}-inspect-patched`, bootDisk.image); + assert.equal(firmware.status, 0, firmware.stderr); + + assert.equal(inspect('data', blankDisk(kind).image), 'desktop=0 driver=0'); + assert.equal(inspect('boot', bootDisk.image), 'desktop=1 driver=0'); + assert.equal(inspect('patched', fs.readFileSync(firmware.out)), 'desktop=1 driver=1'); + }); + } + + await t.test('d71: refuses a single-sided disk', () => { + const disk = blankDisk('d71'); + disk.image[offset(disk.format, 18, 0) + 3] = 0; + const { python, firmware } = patchBoth(dir, 'd71-single', disk.image); + assert.equal(firmware.status, 1); + assert.equal(firmware.stderr.trim(), python.stderr.trim()); + }); +}); diff --git a/Source/Teensy/tests/geos_disk_patch_harness.c b/Source/Teensy/tests/geos_disk_patch_harness.c new file mode 100644 index 00000000..6d369821 --- /dev/null +++ b/Source/Teensy/tests/geos_disk_patch_harness.c @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +// geos_disk_patch_harness IN OUT: the firmware's GEOS boot-disk patch, run on an image file. +// Exits 0 after writing OUT, 1 with the patcher's refusal on stderr, 3 if a refusal changed the image. +// geos_disk_patch_harness --inspect IN: prints what GeosInspect found, or exits 1 with its refusal. +#include + +#include "../GEOS/GeosTargets.h" + +#define MAX_IMAGE 819200 + +// One byte past the largest image, so an oversized file reads as a size no format has +static uint8_t Image[MAX_IMAGE + 1], Original[MAX_IMAGE + 1]; + +static bool ReadBlock(void *Context, uint32_t Offset, uint8_t *Block) +{ + memcpy(Block, (uint8_t *)Context + Offset, GEOS_SECTOR_SIZE); + return true; +} + +static bool WriteBlock(void *Context, uint32_t Offset, const uint8_t *Block) +{ + memcpy((uint8_t *)Context + Offset, Block, GEOS_SECTOR_SIZE); + return true; +} + +static int Inspect(struct GeosDisk *Disk) +{ + struct GeosBootDisk Found; + + if (!GeosInspect(Disk, &Found)) + { + fprintf(stderr, "%s\n", Disk->Error); + return 1; + } + printf("desktop=%d driver=%d\n", Found.HasDeskTop, Found.HasDriver); + return 0; +} + +int main(int argc, char **argv) +{ + const struct GeosDate Date = { 26, 10, 16, 12, 0 }; + struct GeosDisk Disk = { 0 }; + FILE *In, *Out; + size_t Size; + bool Inspecting; + + if (argc != 3) return 2; + Inspecting = !strcmp(argv[1], "--inspect"); + In = fopen(Inspecting ? argv[2] : argv[1], "rb"); + if (!In) return 2; + Size = fread(Image, 1, sizeof Image, In); + fclose(In); + memcpy(Original, Image, Size); + + Disk.Format = GeosFormatForSize(Size); + Disk.Target = &Geos128Launcher.Target; + Disk.Context = Image; + Disk.Read = ReadBlock; + Disk.Write = WriteBlock; + if (Inspecting) return Inspect(&Disk); + if (!GeosInstallDriver(&Disk, &Date)) + { + fprintf(stderr, "%s\n", Disk.Error); + return memcmp(Original, Image, Size) ? 3 : 1; + } + + Out = fopen(argv[2], "wb"); + if (!Out || fwrite(Image, 1, Size, Out) != Size) return 2; + fclose(Out); + return 0; +} diff --git a/Source/Teensy/tests/usb-mouse.test.js b/Source/Teensy/tests/usb-mouse.test.js new file mode 100644 index 00000000..cd791b71 --- /dev/null +++ b/Source/Teensy/tests/usb-mouse.test.js @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT +// The USB mouse registers GeoRAM serves at $DF00, held to the register map in USBMouse.c. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; + +import { commandExists } from '../../../tools/lib/toolchain.mjs'; + +const HARNESS_SOURCE = path.join(import.meta.dirname, 'usb_mouse_harness.cpp'); + +const NOT_CI = ['', '0', 'false']; +const inCi = !NOT_CI.includes(process.env.CI ?? ''); +const skip = !commandExists('c++') && !inCi && 'c++ is not on PATH'; + +const X = 0, Y = 1, BUTTONS = 2; +const COUNTS_PER_STEP = 4; +const MAX_STEP = 127; + +let harnessPath; +function harness() { + if (harnessPath) return harnessPath; + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'usb-mouse-')); + harnessPath = path.join(dir, 'harness'); + const built = spawnSync('c++', ['-std=gnu++17', '-Wall', '-Wextra', '-Werror', '-o', harnessPath, HARNESS_SOURCE], { encoding: 'utf8' }); + assert.equal(built.status, 0, built.stderr); + return harnessPath; +} + +function run(lines) { + const ran = spawnSync(harness(), { input: lines.join('\n') + '\n', encoding: 'utf8' }); + assert.equal(ran.status, 0, ran.stderr); + return ran.stdout.split('\n').filter(Boolean); +} + +const report = (dx, dy, buttons = 0) => [`report ${dx} ${dy} ${buttons.toString(16)}`, 'poll']; +const read = (offset) => `r ${offset}`; +const signed = (byte) => (parseInt(byte, 16) << 24) >> 24; + +test('the id is TRM at +3, and nothing answers after it', { skip }, () => { + assert.deepEqual(run([3, 4, 5, 6, 7].map(read)), ['54', '52', '4d', 'open', 'open']); +}); + +test('a large movement arrives over several reads, 127 at most each, then the remainder', { skip }, () => { + const steps = 300; + const got = run([...report(steps * COUNTS_PER_STEP, 0), ...Array(5).fill(read(X))]).map(signed); + assert.deepEqual(got, [MAX_STEP, MAX_STEP, steps - 2 * MAX_STEP, 0, 0]); +}); + +test('X and Y keep their own movement, whatever order they are read in', { skip }, () => { + const got = run([...report(200 * COUNTS_PER_STEP, -150 * COUNTS_PER_STEP), ...[X, Y, Y, X, Y, X, X].map(read)]).map(signed); + assert.deepEqual(got, [127, -127, -23, 73, 0, 0, 0]); +}); + +test('counts below a step carry over to the next report, rounding toward negative', { skip }, () => { + assert.deepEqual(run([...report(3, -1), read(X), read(Y), ...report(1, 0), read(X), read(Y)]).map(signed), [0, -1, 1, 0]); +}); + +// A driver reads +0 and +1 from its interrupt, so any read can land while PollUSBMouse +// is part way through updating the byte the next read will drive. +for (const axis of [X, Y]) { + test(`axis ${axis}: a read landing during the poll's update neither repeats nor loses movement`, { skip }, () => { + const unread = 100, more = 20; + const move = (steps) => report(axis === X ? steps * COUNTS_PER_STEP : 0, axis === Y ? steps * COUNTS_PER_STEP : 0); + const lines = run([...move(unread), `race ${axis}`, ...move(more), ...Array(5).fill(read(axis))]); + assert.equal(lines[0], `race ${unread.toString(16)}`); + assert.deepEqual(lines.slice(1).map(signed), [more, 0, 0, 0, 0]); + }); + + test(`axis ${axis}: a read landing during the poll's update that leaves the same byte ready does not repeat movement`, { skip }, () => { + const steps = 10; + const move = (n) => report(axis === X ? n * COUNTS_PER_STEP : 0, axis === Y ? n * COUNTS_PER_STEP : 0); + const lines = run([...move(steps), `race ${axis}`, ...move(steps), ...Array(3).fill(read(axis))]); + assert.equal(lines[0], `race ${steps.toString(16).padStart(2, '0')}`); + assert.deepEqual(lines.slice(1).map(signed), [steps, 0, 0]); + }); +} + +test('a weak compare-exchange failing for no reason still leaves the new movement ready', { skip }, () => { + assert.deepEqual(run(['spurious 0', ...report(20 * COUNTS_PER_STEP, 0), read(X)]).map(signed), [20]); +}); + +test('the handler starting discards movement from before it, both polled and still in the controller', { skip }, () => { + const polled = report(40 * COUNTS_PER_STEP, 40 * COUNTS_PER_STEP); + const pending = `report ${30 * COUNTS_PER_STEP} ${30 * COUNTS_PER_STEP} 0`; + const got = run([...polled, pending, 'init', 'poll', read(X), read(Y), ...report(COUNTS_PER_STEP, -COUNTS_PER_STEP), read(X), read(Y)]).map(signed); + assert.deepEqual(got, [0, 0, 1, -1]); +}); + +test('the buttons register follows the latest report, and clears when the mouse is unplugged', { skip }, () => { + const got = run([...report(0, 0, 0b101), read(BUTTONS), ...report(0, 0, 0b010), read(BUTTONS), 'unplug', 'poll', read(BUTTONS)]); + assert.deepEqual(got, ['05', '02', '00']); +}); + +test('a report the controller held when the mouse was unplugged is not delivered on replug', { skip }, () => { + const got = run(['unplug', `report ${10 * COUNTS_PER_STEP} 0 1`, 'poll', 'plug', 'poll', read(X), read(BUTTONS)]); + assert.deepEqual(got, ['00', '00']); +}); diff --git a/Source/Teensy/tests/usb_mouse_harness.cpp b/Source/Teensy/tests/usb_mouse_harness.cpp new file mode 100644 index 00000000..16e00c4f --- /dev/null +++ b/Source/Teensy/tests/usb_mouse_harness.cpp @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT +// usb_mouse_harness: the USB mouse registers, driven from stdin one command a line, +// as the USB host, the main loop and the IO ISR would drive them: +// report X Y B the USB host delivers a report into the mouse controller +// unplug, plug the mouse leaves or returns +// poll the main loop's PollUSBMouse +// init InitUSBMouse, as the handler starts +// r OFFSET an IO read, printing the byte driven or "open" +// race AXIS at the next poll's atomic write of AXIS's ready word, an IO read of +// that axis lands first and prints "race BYTE" +// spurious AXIS the next compare-exchange on AXIS's ready word fails without a reason, +// as a weak one may +#include +#include +#include + +#define FLASHMEM +#define NVIC_DISABLE_IRQ(Irq) +#define NVIC_ENABLE_IRQ(Irq) +#define IRQ_USBHS 0 +// Cortex-M barriers do not assemble for the host: asm volatile(...) becomes asm("") +#define volatile(...) ("") + +struct USBHIDInput +{ + bool Plugged = true; + explicit operator bool() { return Plugged; } +}; + +static struct MouseController : USBHIDInput +{ + int X = 0, Y = 0; + uint8_t Buttons = 0; + bool Available = false; + bool available() { return Available; } + int getMouseX() { return X; } + int getMouseY() { return Y; } + uint8_t getButtons() { return Buttons; } + void mouseDataClear() { X = Y = 0; Buttons = 0; Available = false; } +} usbHostMouse; + +static int Driven; +static void DataPortWriteWaitLog(uint8_t Data) { Driven = Data; } + +static int RaceAxis = -1, SpuriousAxis = -1; +static void LandRace(volatile uint32_t *Word); +static bool FailSpuriously(volatile uint32_t *Word, uint32_t *Expected); + +template +static bool RacedCompareExchange(volatile uint32_t *Word, uint32_t *Expected, Rest... rest) +{ + LandRace(Word); + if (FailSpuriously(Word, Expected)) return false; + return __atomic_compare_exchange_n(Word, Expected, rest...); +} + +template +static void RacedStore(volatile uint32_t *Word, Rest... rest) +{ + LandRace(Word); + __atomic_store_n(Word, rest...); +} + +#define __atomic_compare_exchange_n RacedCompareExchange +#define __atomic_store_n RacedStore +#include "../MinimalBoot/Common/IO_Handlers/USBMouse.c" +#undef __atomic_compare_exchange_n +#undef __atomic_store_n +#undef volatile + +static int Read(unsigned Offset) +{ + Driven = -1; + USBMouseIORead(Offset); + return Driven; +} + +static void LandRace(volatile uint32_t *Word) +{ + if (RaceAxis < 0 || Word != &USBMouse_Ready[RaceAxis]) return; + unsigned Axis = RaceAxis; + RaceAxis = -1; + printf("race %02x\n", Read(Axis)); +} + +static bool FailSpuriously(volatile uint32_t *Word, uint32_t *Expected) +{ + if (SpuriousAxis < 0 || Word != &USBMouse_Ready[SpuriousAxis]) return false; + SpuriousAxis = -1; + *Expected = *Word; + return true; +} + +int main() +{ + char Line[64]; + while (fgets(Line, sizeof Line, stdin)) + { + int X, Y; + unsigned Value; + if (sscanf(Line, "report %d %d %x", &X, &Y, &Value) == 3) + { + usbHostMouse.X = X; + usbHostMouse.Y = Y; + usbHostMouse.Buttons = Value; + usbHostMouse.Available = true; + } + else if (!strncmp(Line, "unplug", 6)) usbHostMouse.Plugged = false; + else if (!strncmp(Line, "plug", 4)) usbHostMouse.Plugged = true; + else if (!strncmp(Line, "poll", 4)) PollUSBMouse(); + else if (!strncmp(Line, "init", 4)) InitUSBMouse(); + else if (sscanf(Line, "race %u", &Value) == 1) RaceAxis = Value; + else if (sscanf(Line, "spurious %u", &Value) == 1) SpuriousAxis = Value; + else if (sscanf(Line, "r %x", &Value) == 1) + { + int Byte = Read(Value); + if (Byte < 0) printf("open\n"); + else printf("%02x\n", Byte); + } + else + { + fprintf(stderr, "unknown command: %s", Line); + return 2; + } + } + if (RaceAxis >= 0 || SpuriousAxis >= 0) + { + fprintf(stderr, "an armed race or spurious failure never happened\n"); + return 1; + } + return 0; +} diff --git a/tools/geos/trmouse.py b/tools/geos/trmouse.py new file mode 100644 index 00000000..178d51d0 --- /dev/null +++ b/tools/geos/trmouse.py @@ -0,0 +1,312 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Put the TR USB mouse driver on a GEOS 128 boot disk. + + python3 tools/geos/trmouse.py boot.d81 out.d81 (or a .d64 or .d71) + python3 tools/geos/trmouse.py --c-header out.h (the driver file, for the firmware) + +GEOS loads the first input driver in the boot disk's directory, so the driver +goes into the entry ahead of every other one, and the input driver that was +first moves to the slot the new file would have taken. Needs acme on PATH. +""" + +import argparse +import datetime +import pathlib +import subprocess +import sys +import tempfile + +SOURCE = pathlib.Path(__file__).with_name('trmouse128.asm') +NAME = b'TR USB MOUSE' +CLASS_NAME = b'TR USB Mouse' +CLASS_NAME_SIZE = 12 # GEOS reads the version right after it +LOAD = 0xfd00 +MAX_SIZE = 0x180 # GEOS 128's input driver area, $FD00-$FE7F + +SECTOR_SIZE = 256 +DIR_ENTRY_SIZE = 32 + +USR_CLOSED = 0x83 +GEOS_SEQUENTIAL = 0 +INPUT_128 = 15 +INFO_ICON_BITMAP = 0xbf # an uncompressed 63-byte icon follows + +ICON = """ +........................ +.........######......... +.......##..#...##....... +......#....#.....#...... +.....#.....#......#..... +.....#.....#......#..... +.....#.....#......#..... +.....##############..... +.....#............#..... +.....#............#..... +.....#............#..... +.....#............#..... +.....#............#..... +.....#............#..... +......#..........#...... +.......##......##....... +.........######......... +........................ +...####.####.####....... +.....#..#..#.#..#....... +.....#..###..####....... +""" + + +def assemble(): + with tempfile.TemporaryDirectory() as tmp: + out = pathlib.Path(tmp) / 'trmouse128.bin' + subprocess.run(['acme', '-f', 'plain', '-o', str(out), str(SOURCE)], check=True) + code = out.read_bytes() + if len(code) > MAX_SIZE: + sys.exit(f'the driver is {len(code)} bytes; GEOS 128 has room for {MAX_SIZE}') + return code + + +def icon_bits(): + rows = ICON.split() + assert len(rows) == 21 and all(len(r) == 24 for r in rows) + out = bytearray() + for row in rows: + bits = int(row.replace('#', '1').replace('.', '0'), 2) + out += bits.to_bytes(3, 'big') + return bytes(out) + + +def padded(text, size, pad=b'\0'): + assert len(text) <= size + return text + pad * (size - len(text)) + + +def zone_sectors(track): + """A 1541's sectors per track, which a 1571 repeats on its second side.""" + track = (track - 1) % 35 + 1 + return 21 if track <= 17 else 19 if track <= 24 else 18 if track <= 30 else 17 + + +class Format: + def __init__(self, name, tracks, sectors, dir_track, dir_sectors, reserved, bam): + self.name, self.tracks, self.sectors = name, tracks, sectors + self.dir_track, self.dir_sectors = dir_track, dir_sectors + self.reserved = reserved # tracks DOS never hands out for file data + self.bam = bam # track -> (count, bitmap) as (track, sector, byte) + self.starts = [0] + for track in range(1, tracks + 1): + self.starts.append(self.starts[-1] + sectors(track)) + + @property + def size(self): + return self.starts[-1] * SECTOR_SIZE + + +def d64_bam(track): + return (18, 0, 4 + (track - 1) * 4), (18, 0, 5 + (track - 1) * 4) + + +def d71_bam(track): + if track <= 35: + return d64_bam(track) + return (18, 0, 0xdd + track - 36), (53, 0, (track - 36) * 3) + + +def d81_bam(track): + sector, entry = 1 + (track - 1) // 40, 0x10 + (track - 1) % 40 * 6 + return (40, sector, entry), (40, sector, entry + 1) + + +FORMATS = {f.size: f for f in ( + Format('D64', 35, zone_sectors, 18, range(1, 19), (18,), d64_bam), + Format('D71', 70, zone_sectors, 18, range(1, 19), (18, 53), d71_bam), + Format('D81', 80, lambda track: 40, 40, range(3, 40), (40,), d81_bam), +)} + + +class Disk: + def __init__(self, image): + self.format = FORMATS.get(len(image)) + if self.format is None: + sizes = ', '.join(f'{f.name} {f.size}' for f in FORMATS.values()) + sys.exit(f'not a D64, D71 or D81 without error bytes ({len(image)} bytes; expected {sizes})') + self.image = bytearray(image) + if self.format.name == 'D71' and not self.image[self.offset(18, 0) + 3] & 0x80: + sys.exit('this D71 is formatted single-sided') + + def offset(self, track, sector): + return (self.format.starts[track - 1] + sector) * SECTOR_SIZE + + def at(self, place): + track, sector, byte = place + return self.offset(track, sector) + byte + + def free_sectors(self, track): + _, bitmap = (self.at(p) for p in self.format.bam(track)) + return [s for s in range(self.format.sectors(track)) + if self.image[bitmap + s // 8] & 1 << (s % 8)] + + def take(self, track): + """Mark the first free sector of a track in use; None if it has none.""" + count, bitmap = (self.at(p) for p in self.format.bam(track)) + free = self.free_sectors(track) + if self.image[count] < len(free): + sys.exit(f'the BAM count for track {track} disagrees with its bitmap') + if not free: + return None + sector = free[0] + self.image[bitmap + sector // 8] &= ~(1 << (sector % 8)) + self.image[count] -= 1 + return track, sector + + def allocate(self): + for track in range(1, self.format.tracks + 1): + if track not in self.format.reserved: + found = self.take(track) + if found: + return found + sys.exit('the disk is full') + + def dir_sectors(self): + header = self.offset(self.format.dir_track, 0) + track, sector = self.image[header], self.image[header + 1] + if not track: + sys.exit('the disk header links to no directory') + seen = set() + while track: + in_directory = track == self.format.dir_track and sector in self.format.dir_sectors + if (track, sector) in seen or not in_directory: + sys.exit(f'the directory chain is broken at {track}/{sector}') + seen.add((track, sector)) + yield track, sector + base = self.offset(track, sector) + track, sector = self.image[base], self.image[base + 1] + + def slots(self, track, sector): + base = self.offset(track, sector) + return [base + i * DIR_ENTRY_SIZE for i in range(SECTOR_SIZE // DIR_ENTRY_SIZE)] + + def dir_slots(self): + for track, sector in self.dir_sectors(): + yield from self.slots(track, sector) + + def grow_directory(self): + """Chain one more directory sector, as DOS does when a directory fills; returns its slots.""" + chain = list(self.dir_sectors()) + free = self.free_sectors(self.format.dir_track) + for track, sector in chain: + if sector in free: + sys.exit(f'the BAM shows directory sector {track}/{sector} free') + new = self.take(self.format.dir_track) + if new is None or new[1] not in self.format.dir_sectors: + sys.exit('the directory is full') + o = self.offset(*chain[-1]) + self.image[o:o + 2] = bytes(new) + o = self.offset(*new) + self.image[o:o + SECTOR_SIZE] = padded(b'\x00\xff', SECTOR_SIZE) + return self.slots(*new) + + def write_file(self, data, info): + """Write a GEOS sequential file; returns its first data sector.""" + sectors = [self.allocate() for _ in range((len(data) + 253) // 254)] + for i, (track, sector) in enumerate(sectors): + chunk = data[i * 254:(i + 1) * 254] + if i + 1 < len(sectors): + link = bytes(sectors[i + 1]) + else: + link = bytes((0, len(chunk) + 1)) + o = self.offset(track, sector) + self.image[o:o + SECTOR_SIZE] = padded(link + chunk, SECTOR_SIZE) + info_ts = self.allocate() + o = self.offset(*info_ts) + self.image[o:o + SECTOR_SIZE] = info + return sectors[0], info_ts, len(sectors) + 1 + + +def info_block(size): + info = bytearray(256) + info[0:2] = b'\x00\xff' + info[2:5] = bytes((3, 21, INFO_ICON_BITMAP)) + info[5:0x44] = icon_bits() + info[0x44:0x47] = bytes((USR_CLOSED, INPUT_128, GEOS_SEQUENTIAL)) + info[0x47:0x49] = LOAD.to_bytes(2, 'little') + info[0x49:0x4b] = (LOAD + size).to_bytes(2, 'little') + info[0x4d:0x61] = padded(padded(CLASS_NAME, CLASS_NAME_SIZE, b' ') + b'V1.0', 20) + info[0x61:0x75] = padded(b'TeensyROM', 20) + info[0xa0:0x100] = padded(b'A USB mouse plugged into a TeensyROM.', 0x60) + return bytes(info) + + +def install(image, code): + disk = Disk(image) + slots = list(disk.dir_slots()) + entries = [s for s in slots if disk.image[s + 2] & 7] + if any(disk.image[s + 5:s + 21].rstrip(b'\xa0') == NAME for s in entries): + sys.exit(f'{NAME.decode()} is already on this disk') + free = next((s for s in slots if disk.image[s + 2] == 0), None) + if free is None: + grown = disk.grow_directory() + slots += grown + free = grown[0] + + first, info_ts, blocks = disk.write_file(code, info_block(len(code))) + now = datetime.datetime.now() + entry = bytearray(30) + entry[0] = USR_CLOSED + entry[1:3] = bytes(first) + entry[3:19] = padded(NAME, 16, b'\xa0') + entry[19:21] = bytes(info_ts) + entry[21:23] = bytes((GEOS_SEQUENTIAL, INPUT_128)) + entry[23:28] = bytes((now.year % 100, now.month, now.day, now.hour, now.minute)) + entry[28:30] = blocks.to_bytes(2, 'little') + disk.image[free + 2:free + 32] = entry + + drivers = [s for s in entries if disk.image[s + 24] == INPUT_128] + if drivers and slots.index(drivers[0]) < slots.index(free): + a, b = drivers[0], free + disk.image[a + 2:a + 32], disk.image[b + 2:b + 32] = \ + disk.image[b + 2:b + 32], disk.image[a + 2:a + 32] + return bytes(disk.image) + + +def c_array(name, data): + rows = (', '.join(f'0x{b:02x}' for b in data[i:i + 16]) for i in range(0, len(data), 16)) + body = ',\n'.join(f' {row}' for row in rows) + return f'static const uint8_t {name}[{len(data)}] PROGMEM = {{\n{body}\n}};\n' + + +def c_header(code): + return ( + '// SPDX-License-Identifier: MIT\n' + '// Generated by tools/geos/trmouse.py --c-header from tools/geos/trmouse128.asm; do not edit.\n' + '#pragma once\n' + '#include \n' + '#ifdef ARDUINO\n' + '#include \n' + '#else\n' + '#define PROGMEM\n' + '#endif\n\n' + f'static const char TRMouse128Name[] PROGMEM = "{NAME.decode()}";\n' + + c_array('TRMouse128Code', code) + + c_array('TRMouse128Info', info_block(len(code)))) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument('disk', type=pathlib.Path, nargs='?') + parser.add_argument('out', type=pathlib.Path, nargs='?') + parser.add_argument('--c-header', type=pathlib.Path, metavar='OUT') + args = parser.parse_args() + if args.c_header: + if args.disk: + parser.error('--c-header takes no disk images') + args.c_header.write_bytes(c_header(assemble()).encode()) + return + if not args.out: + parser.error('needs a boot disk and an output image') + args.out.write_bytes(install(args.disk.read_bytes(), assemble())) + + +if __name__ == '__main__': + main() diff --git a/tools/geos/trmouse.test.mjs b/tools/geos/trmouse.test.mjs new file mode 100644 index 00000000..ed13d1ca --- /dev/null +++ b/tools/geos/trmouse.test.mjs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { commandExists } from '../lib/toolchain.mjs'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); +const trmouse = path.join(root, 'tools/geos/trmouse.py'); +const committedHeader = path.join(root, 'Source/Teensy/GEOS/TRMouse128.h'); + +const NOT_CI = ['', '0', 'false']; +const inCi = !NOT_CI.includes(process.env.CI ?? ''); +const missingTool = ['acme', 'python3'].find((tool) => !commandExists(tool)); + +test('the committed driver header matches the driver source', { skip: missingTool && !inCi && `${missingTool} is not on PATH` }, () => { + assert.equal(missingTool, undefined, 'CI installs ACME and Python, so this test must not skip there'); + const out = fs.mkdtempSync(path.join(os.tmpdir(), 'trmouse-test-')); + try { + const header = path.join(out, 'TRMouse128.h'); + const r = spawnSync('python3', [trmouse, '--c-header', header], { encoding: 'utf8' }); + assert.equal(r.status, 0, r.stdout + r.stderr); + const committed = fs.readFileSync(committedHeader, 'utf8').replaceAll('\r\n', '\n'); + assert.equal(fs.readFileSync(header, 'utf8'), committed, 'rerun tools/geos/trmouse.py --c-header'); + } finally { + fs.rmSync(out, { recursive: true, force: true }); + } +}); diff --git a/tools/geos/trmouse128.asm b/tools/geos/trmouse128.asm new file mode 100644 index 00000000..cc6f1d3e --- /dev/null +++ b/tools/geos/trmouse128.asm @@ -0,0 +1,148 @@ +; SPDX-License-Identifier: MIT +; +; GEOS 128 input driver for a USB mouse on a TeensyROM (USBMouse.c has the registers). + +!cpu 6502 + +mouseOn = $30 ; bit 7: the pointer is on +pressFlag = $39 +mouseXPos = $3a ; 16 bits +mouseYPos = $3c +graphMode = $3f ; bit 7: 80 columns +mouseData = $8505 ; bit 7: button up +inputData = $8506 ; a joystick direction, or $ff + +MOUSE_BIT = $20 ; in pressFlag: mouseData changed +BUTTON_UP = $80 +NO_DIRECTION = $ff +OFF_TOP = 228 ; the kernal reads a mouseYPos this high as above the top + +regs = $df00 +regX = 0 +regY = 1 +regButtons = 2 +regID = 3 +LEFT_BUTTON = $01 + + * = $fd00 ; where GEOS 128 loads an input driver + + jmp MouseInit + jmp SlowMouse + jmp UpdateMouse + jmp SetMouse + +lastButton !byte BUTTON_UP +deltaX !word 0 +deltaY !byte 0 + +MouseInit + lda #0 + sta mouseXPos+1 + lda #8 + sta mouseXPos + sta mouseYPos +SlowMouse +SetMouse + rts + +; The ID tells whether the registers are there, beside GeoRAM. +; $DF03-$DF05 are REU address registers, which have no side effects when read. +; Carry set: what reads there is not a mouse. +FindRegs + ldx #2 +- lda regs+regID,x + cmp trID,x + bne + + dex + bpl - + clc + rts ++ sec + rts + +trID !text "TRM" + +UpdateMouse + jsr FindRegs + bcs noMouse + lda regs+regX + sta deltaX + lda regs+regY + sta deltaY + bit mouseOn + bmi readButtons +noMouse rts ; movement while the pointer is off is dropped + +readButtons + lda regs+regButtons + and #LEFT_BUTTON + beq + + lda #0 ; pressed + beq ++ ++ lda #BUTTON_UP +++ cmp lastButton + beq ++ + sta lastButton + sta mouseData + lda pressFlag + ora #MOUSE_BIT + sta pressFlag +++ + + ldx #0 + lda deltaX + bpl + + dex ++ stx deltaX+1 + bit graphMode + bpl + + asl deltaX ; 640 pixels across in 80 columns + rol deltaX+1 ++ clc + lda mouseXPos + adc deltaX + sta mouseXPos + lda mouseXPos+1 + adc deltaX+1 + sta mouseXPos+1 ; the kernal clamps it to the screen + + lda deltaY + clc + adc mouseYPos + bit deltaY + bmi up + bcs bottom + cmp #OFF_TOP + bcc storeY +bottom lda #OFF_TOP-1 ; below the bottom, for the kernal to clamp + bne storeY +up bcs storeY + lda #$ff ; above the top +storeY sta mouseYPos + +; inputData: 0 right, 1 up-right, 2 up ... 7 down-right, as a joystick driver gives + ldx #1 ; column: 0 left, 1 none, 2 right + lda deltaX+1 + bmi + + ora deltaX + beq ++ + inx + bne ++ ++ dex +++ lda deltaY ; row: 0 up, 1 none, 2 down + beq + + bmi up1 + inx + inx + inx ++ inx + inx + inx +up1 lda directions,x + sta inputData + rts + +directions + !byte 3, 2, 1 ; up + !byte 4, NO_DIRECTION, 0 + !byte 5, 6, 7 ; down