diff --git a/Source/Teensy/DriveDirLoad.ino b/Source/Teensy/DriveDirLoad.ino index f8e926c3..d3e7be7e 100644 --- a/Source/Teensy/DriveDirLoad.ino +++ b/Source/Teensy/DriveDirLoad.ino @@ -286,16 +286,30 @@ FLASHMEM void HandleExecution() Printf_dbg("\n Chp# Length Type Bank Addr Size\n"); while (MenuSelCpy.Code_Image + MenuSelCpy.Size - ptrChipOffset > 1) //allow for off by 1 sometimes caused by bin2header { - if (!ParseChipHeader(ptrChipOffset, "")) //sends error messages + const size_t Remaining = MenuSelCpy.Code_Image + MenuSelCpy.Size - ptrChipOffset; + if (Remaining < CRT_CHIP_HDR_LEN || + toU16(ptrChipOffset+0x0E) > Remaining-CRT_CHIP_HDR_LEN) + { + SendMsgPrintfln("Truncated CRT CHIP packet"); + FreeCrtChips(); + IO1[rwRegNextIOHndlr] = EEPROM.read(eepAdNextIOHndlr); + return; + } + const uint16_t PayloadSize = toU16(ptrChipOffset+0x0E); + const int16_t Chip = ParseChipHeader(ptrChipOffset, ""); + if (Chip == -1) //sends error messages { FreeCrtChips(); IO1[rwRegNextIOHndlr] = EEPROM.read(eepAdNextIOHndlr); //in case it was over-ridden by .crt return; } ptrChipOffset += CRT_CHIP_HDR_LEN; - memcpy(CrtChips[NumCrtChips].ChipROM, ptrChipOffset, CrtChips[NumCrtChips].ROMSize); - ptrChipOffset += CrtChips[NumCrtChips].ROMSize; - NumCrtChips++; + if (Chip >= 0) + { + memcpy(CrtChips[Chip].ChipROM, ptrChipOffset, PayloadSize); + if (Chip == NumCrtChips) NumCrtChips++; + } + ptrChipOffset += PayloadSize; } //check configuration (sends error messages) @@ -513,7 +527,12 @@ bool LoadFile(FS *sourceFS, const char* FilePath, StructMenuItem* MyMenuItem) } //load header and parse - for (count = 0; count < CRT_MAIN_HDR_LEN; count++) lclBuf[count]=myFile.read(); //Read main header + if (myFile.read(lclBuf, CRT_MAIN_HDR_LEN) != CRT_MAIN_HDR_LEN) + { + SendMsgPrintfln("Truncated CRT header"); + myFile.close(); + return false; + } MyMenuItem->Code_Image = lclBuf; if (!ParseCRTHeader(MyMenuItem, &EXROM, &GAME)) //sends error messages { @@ -527,8 +546,20 @@ bool LoadFile(FS *sourceFS, const char* FilePath, StructMenuItem* MyMenuItem) Printf_dbg("\n Chp# Length Type Bank Addr Size\n"); while (myFile.available()) { - for (count = 0; count < CRT_CHIP_HDR_LEN; count++) lclBuf[count]=myFile.read(); //Read chip header - if (!ParseChipHeader(lclBuf, FullFilePath)) //sends error messages + if (myFile.size() - myFile.position() < CRT_CHIP_HDR_LEN || + myFile.read(lclBuf, CRT_CHIP_HDR_LEN) != CRT_CHIP_HDR_LEN || + toU16(lclBuf+0x0E) > myFile.size() - myFile.position()) + { + SendMsgPrintfln("Truncated CRT CHIP packet"); + myFile.close(); + FreeCrtChips(); + IO1[rwRegNextIOHndlr] = EEPROM.read(eepAdNextIOHndlr); + RedirectEmptyDriveDirMenu(); + return false; + } + const uint16_t PayloadSize = toU16(lclBuf+0x0E); + const int16_t Chip = ParseChipHeader(lclBuf, FullFilePath); + if (Chip == -1) //sends error messages { myFile.close(); FreeCrtChips(); @@ -536,8 +567,26 @@ bool LoadFile(FS *sourceFS, const char* FilePath, StructMenuItem* MyMenuItem) RedirectEmptyDriveDirMenu(); return false; } - for (count = 0; count < CrtChips[NumCrtChips].ROMSize; count++) CrtChips[NumCrtChips].ChipROM[count]=myFile.read();//read in ROM info: - NumCrtChips++; + if (Chip == -2) + { + if (myFile.seek(myFile.position() + PayloadSize)) continue; + SendMsgPrintfln("Cannot skip CRT CHIP payload"); + myFile.close(); + FreeCrtChips(); + IO1[rwRegNextIOHndlr] = EEPROM.read(eepAdNextIOHndlr); + RedirectEmptyDriveDirMenu(); + return false; + } + if (Chip == NumCrtChips) NumCrtChips++; + if (myFile.read(CrtChips[Chip].ChipROM, PayloadSize) != PayloadSize) + { + SendMsgPrintfln("Truncated CRT CHIP payload"); + myFile.close(); + FreeCrtChips(); + IO1[rwRegNextIOHndlr] = EEPROM.read(eepAdNextIOHndlr); + RedirectEmptyDriveDirMenu(); + return false; + } } //check configuration diff --git a/Source/Teensy/FileParsers.ino b/Source/Teensy/FileParsers.ino index 594ed164..919288c8 100644 --- a/Source/Teensy/FileParsers.ino +++ b/Source/Teensy/FileParsers.ino @@ -20,6 +20,7 @@ uint8_t *ptrRAM_ImageEnd = NULL; // Made global for REU access uint8_t NumCrtChips = 0; StructCrtChip CrtChips[MAX_CRT_CHIPS]; +bool LoadedEasyFlashROM = false; char* StrSIDInfo; // allocated to RAM2 via StrSIDInfoSize char* LatestSIDLoaded; // allocated to RAM2 via MaxPathLength char StrMachineInfo[21] = ""; //~3 extra @@ -52,6 +53,11 @@ bool ParseCRTHeader(StructMenuItem* MyMenuItem, uint8_t *EXROM, uint8_t *GAME) // https://vice-emu.sourceforge.io/vice_17.html#SEC369 // http://ist.uwaterloo.ca/~schepers/formats/CRT.TXT + if (MyMenuItem->Size < CRT_MAIN_HDR_LEN) + { + SendMsgPrintfln("Truncated CRT header"); + return false; + } uint8_t* CRT_Image = MyMenuItem->Code_Image; SendMsgPrintfln("Parsing CRT File"); @@ -74,6 +80,7 @@ bool ParseCRTHeader(StructMenuItem* MyMenuItem, uint8_t *EXROM, uint8_t *GAME) SendMsgPrintfln("Ver: %02x.%02x", CRT_Image[0x14], CRT_Image[0x15]); uint16_t HWType = toU16(CRT_Image+0x16); + LoadedEasyFlashROM = !CrtIsC128 && HWType == Cart_EasyFlash; SendMsgPrintfln("HW Type: %d ($%04x)", (int16_t)HWType, HWType); if (!C128HWTypeSupported(HWType)) return false; @@ -117,128 +124,156 @@ bool ParseCRTHeader(StructMenuItem* MyMenuItem, uint8_t *EXROM, uint8_t *GAME) return true; } -bool ParseChipHeader(uint8_t* ChipHeader, const char *FullFilePath) +FLASHMEM bool BootCrtFromSD(const char *FullFilePath) { - - if (memcmp(ChipHeader, "CHIP", 4)!=0) + if (FullFilePath[0] == 0) { - SendMsgPrintfln("\"CHIP\" not found in #%d", NumCrtChips); + SendMsgPrintfln("Must run this crt from SD"); + return false; + } + //the EEPROM slot is MaxPathLength, shorter than FullFilePath + if (strlen(FullFilePath) >= MaxPathLength) + { + SendMsgPrintfln("CRT path too long\r\nto boot from SD"); return false; } - - //clutters C64 disaply, just send to serial/debug - Printf_dbg(" #%03d $%08x $%04x $%04x $%04x $%04x in RAM", - NumCrtChips, toU32(ChipHeader+0x04), toU16(ChipHeader+0x08), toU16(ChipHeader+0x0A), - toU16(ChipHeader+0x0C), toU16(ChipHeader+0x0E)); - - CrtChips[NumCrtChips].BankNum = toU16(ChipHeader+0x0A); - CrtChips[NumCrtChips].LoadAddress = toU16(ChipHeader+0x0C); - CrtChips[NumCrtChips].ROMSize = toU16(ChipHeader+0x0E); - - //chips in main buffer, then malloc in RAM2. Drop Directory names if space needed? - if (NumCrtChips == 0) ptrRAM_ImageEnd = RAM_Image; //init RAM1 Buffer pointer - //First try RAM1: - if (CrtChips[NumCrtChips].ROMSize + (uint32_t)ptrRAM_ImageEnd - (uint32_t)RAM_Image <= RAM_ImageSize) + //check for minimal image present by seeing if there's an image (this one) at higher address + // These two image checks provided by AndyA via the PJRC forum + // https://forum.pjrc.com/index.php?threads/teensy-4-1-dual-boot-capability.74479/post-339451 + uint32_t imageStartAddress = FLASH_BASEADDRESS + UpperAddr; //point to main TR image + + // check For Valid Image: SPIFlashConfigMagicWord and VectorTableMagicWord + if ((*((uint32_t*)imageStartAddress) != 0x42464346) || (*((uint32_t*)(imageStartAddress+0x1000)) != 0x432000D1)) + { + SendMsgPrintfln("Dual boot image not found (NoMagNums)"); + return false; + } + + // ivt starts 0x1000 after the start of flash. Address of start of code is 2nd vector in table. + uint32_t firstInstructionPtr = imageStartAddress + 0x1000 + sizeof(uint32_t); + uint32_t firstInstructionAddr = *(uint32_t*)firstInstructionPtr; + + // very basic sanity check, code should start after the ivt but not too far into the image. + Printf_dbg("First instruction ptr: 0x%08X addr: 0x%08X\n", firstInstructionPtr, firstInstructionAddr); + if ( (firstInstructionAddr < (imageStartAddress+0x1000)) || (firstInstructionAddr > (imageStartAddress+0x3000)) ) + { + // Address of first instruction isn't sensible for location in flash. Image was probably incorrectly built + SendMsgPrintfln("Dual boot image not found (NoFirstInst)"); + return false; + } + + SendMsgPrintfln("Rebooting Teensy to Minimal"); + EEPwriteStr(eepAdCrtBootName, FullFilePath); + EEPROM.write(eepAdMinBootInd, MinBootInd_ExecuteMin); + #ifdef Fab04_FullDMACapable + // Asked first because this path is reached over USB as well as from the menu -- RemoteControl.ino's + // forced CRT launch calls HandleExecution. Not with the C64 switched off, + // though: that powers the board too, so there is nothing left to reach it. + // What is left is a C64 that still supplies 5 V but has stopped clocking. + // + // Against a bus that has already stopped, the check buys latency and not + // survival. WaitForDMAState bounds the waits it runs itself -- 5 mS after the + // bus goes quiet, or the caller's ceiling while it is still clocking -- so + // PerformDMA and CloseDMA return false on their own, both return values are + // ignored here, and RebootTR() below runs either way. Asking first costs + // nothing on a live bus and, on a dead one, spends 5 mS to skip ~20 mS of DMA + // that could not have worked: each of the two waits pays 5 mS to notice the + // silence and another 5 mS inside AbortDMAUnless. + // + // Against a bus that stops part way through, it is not only latency, which is + // why this is a check and not a deleted line. DMATransferISR's edge waits have + // no bound at all -- see the fourth case named over WaitForDMAState -- so a + // clock that dies while the ISR is inside one spins it forever at priority 16, + // thread mode never runs again, and RebootTR() is never reached. Asking cannot + // close that window, only decline to open it on a bus that already reads dead. + // Fixed 0x00, not read-modify-write: DEN=0 stops all VIC-II byte fetches + //robust for the large majority of real CRT files, with one narrow, named exception: + // an Ultimax-mode cartridge whose own startup code doesn't set $D011. + // If we ever hit that specific case, the fix would need to be different (e.g., detect Ultimax mode from the header and skip the blank) + if (C64IsClockingPHI2()) + { + uint8_t BlankD011 = 0x00; + PerformDMA(DMA_WRITE, 0xD011, &BlankD011, 1, DMA_ADDR_INCREMENT); + CloseDMA(); + } + #endif + RebootTR(); + return false; // RebootTR does not return on hardware. +} + +// -1 refuses the file, -2 skips an unsupported EasyFlash CHIP type; +// otherwise the returned slot receives this payload (duplicates reuse a slot). +int16_t ParseChipHeader(uint8_t* ChipHeader, const char *FullFilePath) +{ + if (memcmp(ChipHeader, "CHIP", 4)!=0) + { + SendMsgPrintfln("\"CHIP\" not found in #%d", NumCrtChips); + return -1; + } + const uint16_t Bank = toU16(ChipHeader+0x0A); + const uint16_t Address = toU16(ChipHeader+0x0C); + const uint16_t Size = toU16(ChipHeader+0x0E); + if (LoadedEasyFlashROM) + { + const uint16_t Type = toU16(ChipHeader+0x08); + if (Type != 0 && Type != 2) return -2; + if (Bank >= 256) + { + SendMsgPrintfln("EasyFlash bank out of range: %u", Bank); + return -1; + } + if (Size != 8192) + { + SendMsgPrintfln("EasyFlash CHIP must be 8 KB"); + return -1; + } + if (Address != 0x8000 && Address != 0xa000 && Address != 0xe000) + { + SendMsgPrintfln("Unsupported EasyFlash CHIP address: $%04x", Address); + return -1; + } + // A small sparse file can use extended banks without exhausting RAM. + if (Bank >= 64) + { + BootCrtFromSD(FullFilePath); + return -1; + } + for (uint8_t Previous=0; Previous= MAX_CRT_CHIPS) + { + SendMsgPrintfln("More than %d CRT Chips found", MAX_CRT_CHIPS); + return -1; + } + CrtChips[NumCrtChips].BankNum = Bank; + CrtChips[NumCrtChips].LoadAddress = Address; + CrtChips[NumCrtChips].ROMSize = Size; + if (NumCrtChips == 0) ptrRAM_ImageEnd = RAM_Image; + if (Size + (uint32_t)ptrRAM_ImageEnd - (uint32_t)RAM_Image <= RAM_ImageSize) { CrtChips[NumCrtChips].ChipROM = ptrRAM_ImageEnd; - ptrRAM_ImageEnd += CrtChips[NumCrtChips].ROMSize; - Printf_dbg("1"); + ptrRAM_ImageEnd += Size; } - else //then try RAM2 + else { - while(NULL == (CrtChips[NumCrtChips].ChipROM = (uint8_t*)malloc(CrtChips[NumCrtChips].ROMSize))) + while(NULL == (CrtChips[NumCrtChips].ChipROM = (uint8_t*)malloc(Size))) { if (DriveDirMenu == NULL) - { //we've used up all of ram1 & 2 now, too big for main image, have to boot minimal image - //SendMsgPrintfln("Too large for main image..."); - - //check for minimal image present by seeing if there's an image (this one) at higher address - // These two image checks provided by AndyA via the PJRC forum - // https://forum.pjrc.com/index.php?threads/teensy-4-1-dual-boot-capability.74479/post-339451 - uint32_t imageStartAddress = FLASH_BASEADDRESS + UpperAddr; //point to main TR image - - // check For Valid Image: SPIFlashConfigMagicWord and VectorTableMagicWord - if ((*((uint32_t*)imageStartAddress) != 0x42464346) || (*((uint32_t*)(imageStartAddress+0x1000)) != 0x432000D1)) - { - SendMsgPrintfln("Dual boot image not found (NoMagNums)"); - return false; - } - - // ivt starts 0x1000 after the start of flash. Address of start of code is 2nd vector in table. - uint32_t firstInstructionPtr = imageStartAddress + 0x1000 + sizeof(uint32_t); - uint32_t firstInstructionAddr = *(uint32_t*)firstInstructionPtr; - - // very basic sanity check, code should start after the ivt but not too far into the image. - Printf_dbg("First instruction ptr: 0x%08X addr: 0x%08X\n", firstInstructionPtr, firstInstructionAddr); - if ( (firstInstructionAddr < (imageStartAddress+0x1000)) || (firstInstructionAddr > (imageStartAddress+0x3000)) ) - { - // Address of first instruction isn't sensible for location in flash. Image was probably incorrectly built - SendMsgPrintfln("Dual boot image not found (NoFirstInst)"); - return false; - } - - if (FullFilePath[0] == 0) - { - SendMsgPrintfln("Must run this crt from SD"); - return false; - } - //the EEPROM slot is MaxPathLength, shorter than FullFilePath - if (strlen(FullFilePath) >= MaxPathLength) - { - SendMsgPrintfln("CRT path too long\r\nto boot from SD"); - return false; - } - - SendMsgPrintfln("Rebooting Teensy to Minimal"); - EEPwriteStr(eepAdCrtBootName, FullFilePath); - EEPROM.write(eepAdMinBootInd, MinBootInd_ExecuteMin); -#ifdef Fab04_FullDMACapable - // Asked first because this path is reached over USB as well as from the menu -- RemoteControl.ino's - // forced CRT launch calls HandleExecution. Not with the C64 switched off, - // though: that powers the board too, so there is nothing left to reach it. - // What is left is a C64 that still supplies 5 V but has stopped clocking. - // - // Against a bus that has already stopped, the check buys latency and not - // survival. WaitForDMAState bounds the waits it runs itself -- 5 mS after the - // bus goes quiet, or the caller's ceiling while it is still clocking -- so - // PerformDMA and CloseDMA return false on their own, both return values are - // ignored here, and RebootTR() below runs either way. Asking first costs - // nothing on a live bus and, on a dead one, spends 5 mS to skip ~20 mS of DMA - // that could not have worked: each of the two waits pays 5 mS to notice the - // silence and another 5 mS inside AbortDMAUnless. - // - // Against a bus that stops part way through, it is not only latency, which is - // why this is a check and not a deleted line. DMATransferISR's edge waits have - // no bound at all -- see the fourth case named over WaitForDMAState -- so a - // clock that dies while the ISR is inside one spins it forever at priority 16, - // thread mode never runs again, and RebootTR() is never reached. Asking cannot - // close that window, only decline to open it on a bus that already reads dead. - // Fixed 0x00, not read-modify-write: DEN=0 stops all VIC-II byte fetches - //robust for the large majority of real CRT files, with one narrow, named exception: - // an Ultimax-mode cartridge whose own startup code doesn't set $D011. - // If we ever hit that specific case, the fix would need to be different (e.g., detect Ultimax mode from the header and skip the blank) - if (C64IsClockingPHI2()) - { - uint8_t BlankD011 = 0x00; - PerformDMA(DMA_WRITE, 0xD011, &BlankD011, 1, DMA_ADDR_INCREMENT); - CloseDMA(); - } -#endif - RebootTR(); - - } - else { - FreeDriveDirMenu(); //free/clear prev loaded directory to make space + BootCrtFromSD(FullFilePath); + return -1; } + FreeDriveDirMenu(); } - Printf_dbg("2"); } - Printf_dbg(" %08x\n", (uint32_t)CrtChips[NumCrtChips].ChipROM); - return true; + return NumCrtChips; } - + FLASHMEM bool ParseARTHeader() { // XferImage and XferSize are populated w/ koala file info @@ -469,6 +504,11 @@ bool PathIsRoot() bool SetTypeFromCRT(StructMenuItem* MyMenuItem, uint8_t EXROM, uint8_t GAME) { SendMsgPrintfln("%d Chip(s) found/loaded", NumCrtChips); + if (!NumCrtChips) + { + SendMsgPrintfln("No supported CRT CHIP packets"); + return false; + } MyMenuItem->Code_Image = CrtChips[0].ChipROM; //check configuration diff --git a/Source/Teensy/MinimalBoot/Common/C128Crt.h b/Source/Teensy/MinimalBoot/Common/C128Crt.h index 8f821cc9..cfb0373a 100644 --- a/Source/Teensy/MinimalBoot/Common/C128Crt.h +++ b/Source/Teensy/MinimalBoot/Common/C128Crt.h @@ -5,9 +5,6 @@ // Included after IOHandlers.h and Common_Defs.h, ahead of the .ino code that calls it: it sets the // cart lines and ROM pointers those define. -extern StructCrtChip CrtChips[]; -extern uint8_t NumCrtChips; - bool CrtIsC128 = false; static C128CartMap C128Map; @@ -27,7 +24,13 @@ static bool C128HWTypeSupported(uint16_t HWType) // The C128 half of SetTypeFromCRT; keeps the map for LaunchC128Cart. static bool SetC128Type(StructMenuItem *MyMenuItem) { +#if defined(MinimumBuild) && !defined(VM_HOST_PROFILE) + // C128 keeps the existing non-EasyFlash limit even in the wider loader. + if (NumCrtChips > MAX_LEGACY_CRT_CHIPS || + !MapC128Cart(CrtChips, (uint8_t)NumCrtChips, &C128Map)) +#else if (!MapC128Cart(CrtChips, NumCrtChips, &C128Map)) +#endif { SendMsgPrintfln("C128 chip layout unsupported"); return false; diff --git a/Source/Teensy/MinimalBoot/Common/CrtChips.h b/Source/Teensy/MinimalBoot/Common/CrtChips.h index 66591334..8ed2e4e5 100644 --- a/Source/Teensy/MinimalBoot/Common/CrtChips.h +++ b/Source/Teensy/MinimalBoot/Common/CrtChips.h @@ -3,6 +3,19 @@ #include #include +#define CRT_MAIN_HDR_LEN 0x40 +#define CRT_CHIP_HDR_LEN 0x10 +#define MAX_LEGACY_CRT_CHIPS 128 +#if defined(MinimumBuild) && !defined(VM_HOST_PROFILE) +#define MAX_CRT_CHIPS 512 +#else +#define MAX_CRT_CHIPS MAX_LEGACY_CRT_CHIPS +#endif +#ifndef SwapSeekAddrMask +// Virtual address marker ORed with an SD-file offset in MinimalBoot. +#define SwapSeekAddrMask 0xef000000 +#endif + // One CHIP packet of a loaded .crt. Pure, so the native tests can hold the layout rules below. struct StructCrtChip { @@ -12,6 +25,15 @@ struct StructCrtChip uint16_t BankNum; }; +#if defined(MinimumBuild) && !defined(VM_HOST_PROFILE) +extern StructCrtChip *CrtChips; +extern uint16_t NumCrtChips; +#else +// Keep the main image's ISR table reads in DTCM and VMBoot's original footprint. +extern StructCrtChip CrtChips[]; +extern uint8_t NumCrtChips; +#endif + struct C128CartMap { const uint8_t *ROML, *ROMH; diff --git a/Source/Teensy/MinimalBoot/Common/DriveDirLoad.h b/Source/Teensy/MinimalBoot/Common/DriveDirLoad.h index 99ddf0ba..d20394b6 100644 --- a/Source/Teensy/MinimalBoot/Common/DriveDirLoad.h +++ b/Source/Teensy/MinimalBoot/Common/DriveDirLoad.h @@ -177,15 +177,10 @@ StructHWID_IOH_Assoc HWID_IOH_Assoc[]= #define StrSIDInfoSize (5*40+5) // max 5 *full* lines + 1 blank line -#define CRT_MAIN_HDR_LEN 0x40 -#define CRT_CHIP_HDR_LEN 0x10 -#define MAX_CRT_CHIPS 128 - -#define SwapSeekAddrMask 0xef000000 //virtual address mask/marker for swap-out blocks, or-ed with file seek address (minimal build only) - - #include "CrtChips.h" +void FreeCrtChips(); + #ifndef MinimumBuild //only needed in full build: struct StructExt_ItemType_Assoc diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_EasyFlash.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_EasyFlash.c index 10288676..f2c7547f 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_EasyFlash.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_EasyFlash.c @@ -18,7 +18,16 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #define Printf_Swaps Serial.printf //Printf_dbg // -#define NumDecodeBanks 64 +#if defined(MinimumBuild) && !defined(VM_HOST_PROFILE) +#define NumDecodeBanks 256 +// Allocated with the EasyFlash CHIP table before payloads fill RAM2. +// ROM reads run in the PHI2 ISR, so this page must never point into flash. +static uint8_t* EasyFlashErasedBank = NULL; +static uint8_t EasyFlashBankMask = 0x3f; +#else +// The main image and the fixed 16 KiB extension carrier retain their budget. +#define NumDecodeBanks 64 +#endif uint8_t *BankDecode[NumDecodeBanks][2]; uint8_t EZFlashRAM[256]; #ifdef FeatVMHost @@ -63,6 +72,57 @@ stcIOHandlers IOHndlr_EasyFlash = }; #ifdef MinimumBuild +#ifndef VM_HOST_PROFILE +static FLASHMEM bool ReadBank(uint32_t SeekTo, uint8_t* ptrImage) +{ + if (!myFile) + { + char FullFilePath[MaxNamePathLength]; + if (PathIsRoot()) snprintf(FullFilePath, sizeof FullFilePath, "%s%s", DriveDirPath, DriveDirMenu.Name); + else snprintf(FullFilePath, sizeof FullFilePath, "%s/%s", DriveDirPath, DriveDirMenu.Name); + myFile = SD.open(FullFilePath, FILE_READ); + if (!myFile) + { + Serial.printf("CRT bank file open failed at $%08lx\n", (unsigned long)SeekTo); + return false; + } + } + if (!myFile.seek(SeekTo)) + { + Serial.printf("CRT bank seek failed at $%08lx\n", (unsigned long)SeekTo); + memset(ptrImage, 0xff, 8192); + return false; + } + int BytesRead = (int)myFile.read(ptrImage, 8192); + if (BytesRead < 0) BytesRead = 0; + if (BytesRead < 8192) + { + Serial.printf("CRT bank read truncated at $%08lx (%d/8192 bytes)\n", (unsigned long)SeekTo, BytesRead); + memset(ptrImage + BytesRead, 0xff, 8192 - BytesRead); + return false; + } + return true; +} + +// Magic Desk 2 retains the shared reader's short-read $FF suffix fallback. +void LoadBank(uint32_t SeekTo, uint8_t* ptrImage) { (void)ReadBank(SeekTo, ptrImage); } + +FLASHMEM void runMainTRApp_FromMin(); +[[noreturn]] static FLASHMEM void EasyFlashSwapFailed(uint32_t Offset) +{ + // Foreground only: hold the C64 in reset before retiring the failed bank. + SetResetAssert; + LOROM_Image = HIROM_Image = NULL; + SetGameDeassert; SetExROMDeassert; + SetDMADeassert; + DMA_State = DMA_S_DisableReady; + doReset = false; + Serial.printf("EasyFlash bank unavailable at $%08lx; returning to menu\n", (unsigned long)Offset); + Serial.flush(); + runMainTRApp_FromMin(); + for (;;) {} // the main image's reset handler does not return +} +#else void LoadBank(uint32_t SeekTo, uint8_t* ptrImage) { if(!myFile) @@ -97,10 +157,31 @@ void LoadBank(uint32_t SeekTo, uint8_t* ptrImage) //Printf_dbg("\n %lu mS Load ", millis()-Startms); } #endif +uint8_t* ImageCheckAssign(uint8_t* BankRequested); +#endif void InitHndlr_EasyFlash() { +#if defined(MinimumBuild) && !defined(VM_HOST_PROFILE) + EasyFlashBankMask = 0x3f; + uint8_t* ErasedBank = EasyFlashErasedBank; + // Sparse native images read as erased flash, including an absent ROMH in bank 0. + for(uint16_t BankNum = 0; BankNum < NumDecodeBanks; BankNum++) {BankDecode[BankNum][0]=ErasedBank;BankDecode[BankNum][1]=ErasedBank;} + for(uint16_t ChipNum = 0; ChipNum < NumCrtChips; ChipNum++) + { + Printf_dbg("Bank# %02d,%d chip# %03d addr %08x\n", CrtChips[ChipNum].BankNum, (CrtChips[ChipNum].LoadAddress == 0x8000 ? 0 : 1), ChipNum, (uint32_t)CrtChips[ChipNum].ChipROM); + if (CrtChips[ChipNum].BankNum < NumDecodeBanks) + { + const uint16_t Bank = CrtChips[ChipNum].BankNum; + if (Bank >= 128) EasyFlashBankMask = 0xff; + else if (Bank >= 64 && EasyFlashBankMask != 0xff) EasyFlashBankMask = 0x7f; + BankDecode[Bank][CrtChips[ChipNum].LoadAddress == 0x8000 ? 0 : 1] = CrtChips[ChipNum].ChipROM; + } + else Serial.printf("Unexp Bank# (%d) in chip %d\n", CrtChips[ChipNum].BankNum, ChipNum); + } + +#else //initialize bank decode pointers for(uint8_t BankNum = 0; BankNum < NumDecodeBanks; BankNum++) {BankDecode[BankNum][0]=NULL;BankDecode[BankNum][1]=NULL;} for(uint8_t ChipNum = 0; ChipNum < NumCrtChips; ChipNum++) @@ -110,13 +191,52 @@ void InitHndlr_EasyFlash() BankDecode[CrtChips[ChipNum].BankNum][CrtChips[ChipNum].LoadAddress == 0x8000 ? 0 : 1] = CrtChips[ChipNum].ChipROM; else Serial.printf("Unexp Bank# (%d) in chip %d\n", CrtChips[ChipNum].BankNum, ChipNum); } - + +#endif memset(EZFlashRAM, 0, 256); #ifdef MinimumBuild +#ifndef VM_HOST_PROFILE //initialize/invalidate swap buffer, pre-load first swappable chips - uint8_t ChipNum = 0; + uint16_t ChipNum = 0; for(uint8_t BuffNum = 0; BuffNum < Num8kSwapBuffers; BuffNum++) + { + SwapBuffers[BuffNum].Offset=0; //default to zero/invalid + // Bank 0 must be resident before the C64 is released, even when CHIP + // packets are unordered and its halves were not loaded into RAM. + uint8_t* StartupHalf = BuffNum < 2 ? BankDecode[0][BuffNum] : NULL; + if (((uint32_t)StartupHalf & SwapSeekAddrMask) == SwapSeekAddrMask) + { + if (!ReadBank((uint32_t)StartupHalf & ~SwapSeekAddrMask, SwapBuffers[BuffNum].Image)) + { + EasyFlashSwapFailed((uint32_t)StartupHalf & ~SwapSeekAddrMask); + } + SwapBuffers[BuffNum].Offset = (uint32_t)StartupHalf; + continue; + } + //find next chip marked as swappable + while (ChipNum < NumCrtChips) + { + if(((uint32_t)CrtChips[ChipNum].ChipROM & SwapSeekAddrMask) == SwapSeekAddrMask && + CrtChips[ChipNum].BankNum != 0) + { + //pre-load and update + if (!ReadBank((uint32_t)CrtChips[ChipNum].ChipROM & ~SwapSeekAddrMask, SwapBuffers[BuffNum].Image)) + { + EasyFlashSwapFailed((uint32_t)CrtChips[ChipNum].ChipROM & ~SwapSeekAddrMask); + } + SwapBuffers[BuffNum].Offset = (uint32_t)CrtChips[ChipNum].ChipROM; + Printf_Swaps("Pre%d: %08x\n", BuffNum, (uint32_t) SwapBuffers[BuffNum].Offset & ~SwapSeekAddrMask); + ChipNum++; + break; //exit while + } + ChipNum++; + } + } +#else + //initialize/invalidate swap buffer, pre-load first swappable chips + uint8_t ChipNum = 0; + for(uint8_t BuffNum = 0; BuffNum < Num8kSwapBuffers; BuffNum++) { SwapBuffers[BuffNum].Offset=0; //default to zero/invalid //find next chip marked as swappable @@ -126,7 +246,7 @@ void InitHndlr_EasyFlash() { //pre-load and update LoadBank((uint32_t)CrtChips[ChipNum].ChipROM & ~SwapSeekAddrMask, SwapBuffers[BuffNum].Image); - SwapBuffers[BuffNum].Offset = (uint32_t)CrtChips[ChipNum].ChipROM; + SwapBuffers[BuffNum].Offset = (uint32_t)CrtChips[ChipNum].ChipROM; Printf_Swaps("Pre%d: %08x\n", BuffNum, (uint32_t) SwapBuffers[BuffNum].Offset & ~SwapSeekAddrMask); ChipNum++; break; //exit while @@ -134,11 +254,18 @@ void InitHndlr_EasyFlash() ChipNum++; } } +#endif + #endif //start with Bank 0: +#if defined(MinimumBuild) && !defined(VM_HOST_PROFILE) + LOROM_Image = ImageCheckAssign(BankDecode[0][0]); + HIROM_Image = ImageCheckAssign(BankDecode[0][1]); +#else LOROM_Image = BankDecode[0][0]; HIROM_Image = BankDecode[0][1]; +#endif //Printf_dbg("Jmp vect: $%02x%02x\n", HIROM_Image[0x1FFD], HIROM_Image[0x1FFC]); if(memcmp(HIROM_Image+0x1800, "eapi", 4)==0) Serial.printf("EAPI found\n"); else Printf_dbg("EAPI *not* found: %d %d %d %d\n", HIROM_Image[0x1800], HIROM_Image[0x1801], HIROM_Image[0x1802], HIROM_Image[0x1803]); @@ -182,7 +309,11 @@ void IO1Hndlr_EasyFlash(uint8_t Address, bool R_Wn) switch (Address) { case 0x00: // Register $DE00 – EasyFlash Bank (write-only) +#if defined(MinimumBuild) && !defined(VM_HOST_PROFILE) + Data &= EasyFlashBankMask; +#else Data &= 0x3f; +#endif #ifdef FeatVMHost CurrentEasyFlashBank = Data; #endif @@ -248,7 +379,13 @@ void PollingHndlr_EasyFlash() if(HIROM_Image && HIROM_Image == SwapBuffers[NextSwapBuffNum].Image) if(++NextSwapBuffNum==Num8kSwapBuffers) NextSwapBuffNum=0; +#ifndef VM_HOST_PROFILE + SwapBuffers[NextSwapBuffNum].Offset = 0; + if (!ReadBank((uint32_t)LOROM_Image & ~SwapSeekAddrMask, SwapBuffers[NextSwapBuffNum].Image)) + EasyFlashSwapFailed((uint32_t)LOROM_Image & ~SwapSeekAddrMask); +#else LoadBank((uint32_t)LOROM_Image & ~SwapSeekAddrMask, SwapBuffers[NextSwapBuffNum].Image); +#endif Printf_Swaps("L%02d: %08x ", NextSwapBuffNum, (uint32_t)LOROM_Image & ~SwapSeekAddrMask); SwapBuffers[NextSwapBuffNum].Offset = (uint32_t)LOROM_Image; LOROM_Image = SwapBuffers[NextSwapBuffNum].Image; @@ -261,7 +398,13 @@ void PollingHndlr_EasyFlash() if(LOROM_Image && LOROM_Image == SwapBuffers[NextSwapBuffNum].Image) if(++NextSwapBuffNum==Num8kSwapBuffers) NextSwapBuffNum=0; +#ifndef VM_HOST_PROFILE + SwapBuffers[NextSwapBuffNum].Offset = 0; + if (!ReadBank((uint32_t)HIROM_Image & ~SwapSeekAddrMask, SwapBuffers[NextSwapBuffNum].Image)) + EasyFlashSwapFailed((uint32_t)HIROM_Image & ~SwapSeekAddrMask); +#else LoadBank((uint32_t)HIROM_Image & ~SwapSeekAddrMask, SwapBuffers[NextSwapBuffNum].Image); +#endif Printf_Swaps("H%02d: %08x ", NextSwapBuffNum, (uint32_t)HIROM_Image & ~SwapSeekAddrMask); SwapBuffers[NextSwapBuffNum].Offset = (uint32_t)HIROM_Image; HIROM_Image = SwapBuffers[NextSwapBuffNum].Image; @@ -273,4 +416,4 @@ void PollingHndlr_EasyFlash() DMA_State = DMA_S_StartDisable; } #endif -} \ No newline at end of file +} diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_MagicDesk.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_MagicDesk.c index a21343f7..85df5516 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_MagicDesk.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_MagicDesk.c @@ -32,8 +32,7 @@ stcIOHandlers IOHndlr_MagicDesk = NULL, //called at the end of EVERY c64 cycle }; -extern StructCrtChip CrtChips[]; -extern uint8_t NumCrtChips; +// CrtChips.h declares the image-specific table and count. uint8_t BankReg; diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c index fe43dcac..269499dc 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c @@ -147,8 +147,7 @@ extern bool DMAByte(uint8_t *Data); extern void FreeDriveDirMenu(); extern uint8_t RAM_Image[]; -extern StructCrtChip CrtChips[]; // Removal breaks downstream code, so keep it here for now. -extern uint8_t NumCrtChips; +// 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 diff --git a/Source/Teensy/MinimalBoot/Min_DriveDirLoad.ino b/Source/Teensy/MinimalBoot/Min_DriveDirLoad.ino index 8a41a628..36138d12 100644 --- a/Source/Teensy/MinimalBoot/Min_DriveDirLoad.ino +++ b/Source/Teensy/MinimalBoot/Min_DriveDirLoad.ino @@ -18,8 +18,33 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#ifdef VM_HOST_PROFILE uint8_t NumCrtChips = 0; StructCrtChip CrtChips[MAX_CRT_CHIPS]; +#else +uint16_t NumCrtChips = 0; +bool LoadedEasyFlashROM = false; +StructCrtChip *CrtChips = NULL; +// Reserve the erased page before CHIP payloads fill RAM2. Other cartridges +// allocate only their original 128 metadata entries and no erased page. +FLASHMEM bool EnsureCrtChipTable() +{ + if (!CrtChips) + { + const uint32_t TableBytes = (LoadedEasyFlashROM ? MAX_CRT_CHIPS : MAX_LEGACY_CRT_CHIPS) * sizeof(StructCrtChip); + uint8_t* Storage = (uint8_t*)calloc(1, TableBytes + (LoadedEasyFlashROM ? 8192u : 0u)); + if (!Storage) + { + SendMsgPrintfln("Not enough RAM for CRT CHIP table%s", LoadedEasyFlashROM ? " and erased page" : ""); + return false; + } + CrtChips = (StructCrtChip*)Storage; + EasyFlashErasedBank = LoadedEasyFlashROM ? Storage + TableBytes : NULL; + if (EasyFlashErasedBank) memset(EasyFlashErasedBank, 0xff, 8192); + } + return true; +} +#endif #include "Common/C128Crt.h" File myFile = NULL; @@ -136,7 +161,16 @@ bool LoadFile(StructMenuItem* MyMenuItem, FS *sourceFS) } //load header and parse +#ifdef VM_HOST_PROFILE for (count = 0; count < CRT_MAIN_HDR_LEN; count++) lclBuf[count]=myFile.read(); //Read main header +#else + if (myFile.read(lclBuf, CRT_MAIN_HDR_LEN) != CRT_MAIN_HDR_LEN) + { + SendMsgPrintfln("Truncated CRT header"); + myFile.close(); + return false; + } +#endif MyMenuItem->Code_Image = lclBuf; if (!ParseCRTHeader(MyMenuItem, &EXROM, &GAME)) //sends error messages { @@ -147,6 +181,7 @@ bool LoadFile(StructMenuItem* MyMenuItem, FS *sourceFS) //process Chip Packets FreeCrtChips(); //clears any previous and resets NumCrtChips Printf_dbg("\n Chp# Length Type Bank Addr Size\n"); +#ifdef VM_HOST_PROFILE while (myFile.available()) { if (NumCrtChips == MAX_CRT_CHIPS) @@ -158,7 +193,7 @@ bool LoadFile(StructMenuItem* MyMenuItem, FS *sourceFS) } for (count = 0; count < CRT_CHIP_HDR_LEN; count++) lclBuf[count]=myFile.read(); //Read chip header - if (!ParseChipHeader(lclBuf)) //sends error messages + if (!ParseLegacyChipHeader(lclBuf)) //sends error messages { myFile.close(); FreeCrtChips(); @@ -187,6 +222,65 @@ bool LoadFile(StructMenuItem* MyMenuItem, FS *sourceFS) NumCrtChips++; } +#else + while (myFile.available()) + { + if (myFile.size() - myFile.position() < CRT_CHIP_HDR_LEN || + myFile.read(lclBuf, CRT_CHIP_HDR_LEN) != CRT_CHIP_HDR_LEN || + toU16(lclBuf+0x0E) > myFile.size() - myFile.position()) + { + SendMsgPrintfln("Truncated CRT CHIP packet at $%08lx", (unsigned long)myFile.position()); + myFile.close(); + FreeCrtChips(); + return false; + } + const int16_t Slot = ParseChipHeader(lclBuf); + if (Slot == -2) + { + // Unsupported EasyFlash CHIP types have no mapping/count effect. + if (myFile.seek(myFile.position() + toU16(lclBuf+0x0E))) continue; + SendMsgPrintfln("CRT CHIP skip seek failed at $%08lx", (unsigned long)myFile.position()); + myFile.close(); FreeCrtChips(); return false; + } + if (Slot < 0) + { + myFile.close(); FreeCrtChips(); return false; + } + // A duplicate half reuses its metadata and, where resident, its RAM. + // A swapped half instead receives the newest packet's file offset. + StructCrtChip& Chip = CrtChips[Slot]; + const bool NewSlot = Slot == NumCrtChips; + if (NewSlot) NumCrtChips++; // cleanup owns the allocation even on read failure + if (Chip.ChipROM == (uint8_t*)SwapSeekAddrMask) + { + // The upper byte is the swap marker, leaving 24 bits for file offsets. + const uint32_t PayloadOffset = myFile.position(); + if (PayloadOffset > 0x00ffffffu || + (Chip.ROMSize && (uint32_t)Chip.ROMSize-1u > 0x00ffffffu-PayloadOffset)) + { + SendMsgPrintfln("CRT CHIP swap offset exceeds 24-bit range at $%08lx", (unsigned long)PayloadOffset); + myFile.close(); FreeCrtChips(); return false; + } + Chip.ChipROM = (uint8_t*)(SwapSeekAddrMask + PayloadOffset); + if (!myFile.seek(myFile.position() + Chip.ROMSize)) + { + SendMsgPrintfln("CRT CHIP seek failed at $%08lx", (unsigned long)myFile.position()); + myFile.close(); FreeCrtChips(); return false; + } + } + else if (myFile.read(Chip.ChipROM, Chip.ROMSize) != Chip.ROMSize) + { + SendMsgPrintfln("Truncated CRT CHIP data at $%08lx", (unsigned long)myFile.position()); + myFile.close(); FreeCrtChips(); return false; + } + Printf_dbg(" %08x\n", (uint32_t)Chip.ChipROM); + } + // Duplicates can replace a swap marker with a resident page or vice + // versa, so decide whether to retain the file from the final mappings. + for (uint16_t ChipNum=0; ChipNumSize < CRT_MAIN_HDR_LEN) { SendMsgPrintfln("Truncated CRT header"); return false; } +#endif uint8_t* CRT_Image = MyMenuItem->Code_Image; SendMsgPrintfln("Parsing CRT File"); @@ -252,6 +349,9 @@ bool ParseCRTHeader(StructMenuItem* MyMenuItem, uint8_t *EXROM, uint8_t *GAME) SendMsgPrintfln("Ver: %02x.%02x", CRT_Image[0x14], CRT_Image[0x15]); uint16_t HWType = toU16(CRT_Image+0x16); +#ifndef VM_HOST_PROFILE + LoadedEasyFlashROM = !CrtIsC128 && HWType == Cart_EasyFlash; +#endif SendMsgPrintfln("HW Type: %d ($%04x)", (int16_t)HWType, HWType); if (!C128HWTypeSupported(HWType)) return false; @@ -274,7 +374,8 @@ bool ParseCRTHeader(StructMenuItem* MyMenuItem, uint8_t *EXROM, uint8_t *GAME) return true; } -bool ParseChipHeader(uint8_t* ChipHeader) +#ifdef VM_HOST_PROFILE +bool ParseLegacyChipHeader(uint8_t* ChipHeader) { static uint8_t *ptrRAM_ImageEnd = NULL; @@ -321,10 +422,78 @@ bool ParseChipHeader(uint8_t* ChipHeader) return true; } +#else +int16_t ParseChipHeader(uint8_t* ChipHeader) +{ + static uint8_t *ptrRAM_ImageEnd = NULL; + if (memcmp(ChipHeader, "CHIP", 4)!=0) + { + SendMsgPrintfln("\"CHIP\" not found in #%d", NumCrtChips); + return -1; + } + const uint16_t Bank = toU16(ChipHeader+0x0A); + const uint16_t Address = toU16(ChipHeader+0x0C); + const uint16_t Size = toU16(ChipHeader+0x0E); + uint16_t Slot = NumCrtChips; +#ifndef VM_HOST_PROFILE + if (LoadedEasyFlashROM) + { + const uint16_t Type = toU16(ChipHeader+0x08); + if (Type != 0 && Type != 2) return -2; + if (Bank >= 256) { SendMsgPrintfln("EasyFlash bank %u exceeds 255", Bank); return -1; } + if (Size != 8192) { SendMsgPrintfln("EasyFlash CHIP size %u is not 8192", Size); return -1; } + if (Address != 0x8000 && Address != 0xa000 && Address != 0xe000) + { + SendMsgPrintfln("EasyFlash CHIP address $%04x unsupported", Address); return -1; + } + // $A000 and $E000 name the same high half. Last accepted packet wins. + for (uint16_t Previous=0; Previous= Capacity) + { + SendMsgPrintfln("More than %u CRT Chips found", Capacity); return -1; + } +#ifndef VM_HOST_PROFILE + if (!EnsureCrtChipTable()) return -1; +#endif + StructCrtChip& Chip = CrtChips[Slot]; + if (Slot == NumCrtChips) + { + if (NumCrtChips == 0) ptrRAM_ImageEnd = RAM_Image; + if (Size + (uint32_t)ptrRAM_ImageEnd - (uint32_t)RAM_Image <= RAM_ImageSize) + { + Chip.ChipROM = ptrRAM_ImageEnd; + ptrRAM_ImageEnd += Size; + } + else + { + Chip.ChipROM = (uint8_t*)malloc(Size); + if (!Chip.ChipROM) Chip.ChipROM = (uint8_t*)SwapSeekAddrMask; + } + } + else if (((uint32_t)Chip.ChipROM & SwapSeekAddrMask) == SwapSeekAddrMask) + Chip.ChipROM = (uint8_t*)SwapSeekAddrMask; + Chip.BankNum = Bank; + Chip.LoadAddress = Address; + Chip.ROMSize = Size; + return Slot; +} + +#endif void FreeCrtChips() { //free chips allocated in RAM2 and reset NumCrtChips for(uint16_t cnt=0; cnt < NumCrtChips; cnt++) if((uint32_t)CrtChips[cnt].ChipROM >= 0x20200000 && (uint32_t)CrtChips[cnt].ChipROM < SwapSeekAddrMask) free(CrtChips[cnt].ChipROM); +#ifndef VM_HOST_PROFILE + free(CrtChips); CrtChips = NULL; + EasyFlashErasedBank = NULL; // the page belongs to the same allocation +#endif NumCrtChips = 0; } @@ -336,6 +505,9 @@ bool PathIsRoot() bool SetTypeFromCRT(StructMenuItem* MyMenuItem, uint8_t EXROM, uint8_t GAME) { SendMsgPrintfln("%d Chip(s) found/loaded", NumCrtChips); +#ifndef VM_HOST_PROFILE + if (!NumCrtChips) { SendMsgPrintfln("No supported CRT Chips found"); return false; } +#endif MyMenuItem->Code_Image = CrtChips[0].ChipROM; //check configuration diff --git a/Source/Teensy/tests/easyflash-fixtures.mjs b/Source/Teensy/tests/easyflash-fixtures.mjs new file mode 100644 index 00000000..95eb4fde --- /dev/null +++ b/Source/Teensy/tests/easyflash-fixtures.mjs @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +// Original bank-identification data, with no game, ROM or copyrighted assets. +// These are loader fixtures, not a runnable C64 test application. +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import crypto from 'node:crypto'; +import { pathToFileURL } from 'node:url'; + +export function makeEasyFlashFixture({ mib = 2, sparse = false, reverse = false } = {}) { + assert([1, 2, 4].includes(mib), 'Capacity must be 1, 2 or 4 MiB'); + const header = Buffer.alloc(64); + header.write('C64 CARTRIDGE '); header.writeUInt32BE(64, 16); + header.writeUInt16BE(0x100, 20); header.writeUInt16BE(32, 22); header[24] = 1; + header.write(`EF ${mib}M BANK IDENTIFIERS`, 32); + const banks = mib * 64, packets = []; + for (let bank = 0; bank < banks; ++bank) for (let half = 0; half < 2; ++half) { + if (sparse && bank !== 0 && bank !== banks - 1 && (half ? bank % 7 === 3 : bank % 11 === 5)) continue; + const packet = Buffer.alloc(16 + 8192); + packet.write('CHIP'); packet.writeUInt32BE(packet.length, 4); packet.writeUInt16BE(2, 8); + packet.writeUInt16BE(bank, 10); packet.writeUInt16BE(half ? 0xa000 : 0x8000, 12); packet.writeUInt16BE(8192, 14); + for (let offset = 0; offset < 8192; ++offset) packet[16 + offset] = bank * 11 + half * 0x59 + offset * 7 + (offset >> 8); + packet[16] = bank; packet[17] = half; packet[18] = bank ^ 0x5a; + packet.write(`BANK ${String(bank).padStart(3, '0')} ROM${half ? 'H' : 'L'}`, 32); + packets.push(packet); + } + return Buffer.concat([header, ...(reverse ? packets.reverse() : packets)]); +} + +if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) { + const options = {}, args = process.argv.slice(2); + while (args.length) { + const arg = args.shift(); + if (arg === '--sparse') options.sparse = true; + else if (arg === '--reverse') options.reverse = true; + else if (arg === '--mib' && args.length) options.mib = Number(args.shift()); + else if (arg === '--output' && args.length) options.output = path.resolve(args.shift()); + else throw Error('Use --mib 2|4 --output fixture.crt [--sparse] [--reverse]'); + } + assert(options.output, 'Supply --output; existing files are never overwritten'); + assert([2, 4].includes(options.mib), 'The public fixture command takes --mib 2 or --mib 4'); + const bytes = makeEasyFlashFixture(options); + fs.writeFileSync(options.output, bytes, { flag: 'wx' }); + console.log(JSON.stringify({ file: options.output, bytes: bytes.length, + sha256: crypto.createHash('sha256').update(bytes).digest('hex'), + banks: options.mib * 64, packets: (bytes.length - 64) / 8208, + sparse: !!options.sparse, reverse: !!options.reverse, + scope: 'bank-identification loader data; not a runnable C64 application' }, null, 2)); +} diff --git a/Source/Teensy/tests/easyflash-harness.cpp b/Source/Teensy/tests/easyflash-harness.cpp new file mode 100644 index 00000000..35b1fb8e --- /dev/null +++ b/Source/Teensy/tests/easyflash-harness.cpp @@ -0,0 +1,354 @@ +// SPDX-License-Identifier: MIT +// Production MinimalBoot + IO handlers, with host-only SD/register/allocation +// stand-ins. The separate main profile tests only routing and its static limit. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#define NOGDI +#include +#else +#include +#endif + +static std::vector fixture; +static std::string messages, bootPath; +static unsigned reads, failReadOn, swaps, returnsToMenu, payloadAllocations; +static bool failSeek, failOpen, shortRead, failMetadata; +static unsigned payloadLimit = 2; +static size_t metadataBudget = SIZE_MAX; +static std::map allocations; +// Keep the production physical-RAM2 ownership check meaningful even with a +// 32-bit host whose ordinary malloc returns addresses below Teensy's RAM2. +static void* arenaAllocate(size_t bytes) { + constexpr size_t arenaBytes = 16 * 1024 * 1024; + static uint8_t* arena = [] { +#ifdef _WIN32 + auto p = VirtualAlloc(reinterpret_cast(0x20200000), arenaBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + auto p = mmap(reinterpret_cast(0x20200000), arenaBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); +#endif + assert(p == reinterpret_cast(0x20200000)); return static_cast(p); + }(); + // Every test releases the preceding cartridge before starting another. + static size_t cursor = 0; + if (allocations.empty()) cursor = 0; + bytes = (bytes + 31) & ~size_t(31); assert(bytes <= arenaBytes - cursor); + void* result = arena + cursor; cursor += bytes; allocations[result] = bytes; return result; +} +struct File { + size_t pos = 0; bool opened = false; + File() = default; File(int) {} + explicit operator bool() const { return opened; } + size_t position() const { return pos; } + size_t size() const { return fixture.size(); } + bool available() const { return opened && pos < fixture.size(); } + int read() { return available() ? fixture[pos++] : -1; } + int read(void* destination, size_t length) { + ++reads; + if (!opened || pos > fixture.size()) return -1; + length = std::min(length, fixture.size() - pos); + if (shortRead || reads == failReadOn) length /= 2; + memcpy(destination, fixture.data() + pos, length); pos += length; return int(length); + } + bool seek(size_t p) { if (failSeek || p > fixture.size()) return false; pos = p; return true; } + void close() { opened = false; } +}; +struct FS { File open(const char*, int) { File file; file.opened = !failOpen; return file; } } SD, USB; +struct SerialMock { + void printf(const char* format, ...) { + char text[2048]; va_list args; va_start(args, format); vsnprintf(text, sizeof text, format, args); va_end(args); messages += text; + } + void println(const char* text) { messages += text; messages += '\n'; } + void flush() {} +} Serial; +static uint8_t RAM_Image[16384]; +static constexpr size_t RAM_ImageSize = sizeof RAM_Image; +static void* hostMalloc(size_t bytes) { + if (payloadAllocations >= payloadLimit) return nullptr; + void* p = arenaAllocate(bytes); ++payloadAllocations; return p; +} +static void* hostCalloc(size_t n, size_t bytes) { + if (failMetadata || n * bytes > metadataBudget) return nullptr; + void* p = arenaAllocate(n * bytes); memset(p, 0, n * bytes); return p; +} +static void hostFree(void* p) { + // Teensy frees RAM2 by its physical address range. On a native host both + // RAM1's fixed array and RAM2 allocations may be above that address. + if (!p || (uintptr_t(p) >= uintptr_t(RAM_Image) && uintptr_t(p) < uintptr_t(RAM_Image + sizeof RAM_Image))) return; + assert(allocations.erase(p) == 1); +} +struct ReturnToMenu {}; +struct BootMinimal {}; +void runMainTRApp_FromMin() { ++returnsToMenu; throw ReturnToMenu{}; } +static bool game, exrom, led, resetHeld, dmaReleased, doReset; +static uint8_t portData, RegNextIOHndlr, CurrentIOHandler; +static const uint8_t *LOROM_Image, *HIROM_Image; +static uint16_t LOROM_Mask, HIROM_Mask; +static void *fBusSnoop, *PendingfBusSnoop; +static unsigned BigBufCount; +volatile uint8_t EmulateVicCycles; +enum { DMA_S_Idle, DMA_S_StartImmediate, DMA_S_ActiveReady, DMA_S_StartDisable, DMA_S_DisableReady }; +static uint8_t DMA_State; +enum { rtUnknown, rtFileCrt, rtBin8kLo, rtBin8kHi, rtBin16k, rtBinC128 }; +enum { IOH_None, IOH_EasyFlash, IOH_MagicDesk2, IOH_Num_Handlers }; +struct StructMenuItem { const char* Name; size_t Size = 0; uint8_t* Code_Image = nullptr; uint8_t ItemType = rtFileCrt; }; +struct stcIOHandlers { + const char* Name; void(*InitHndlr)(); void(*io1)(uint8_t, bool); void(*io2)(uint8_t, bool); + void* lo; void* hi; void(*poll)(); void* cycle; +}; +static uint8_t DataPortWaitRead() { return portData; } +static void DataPortWriteWaitLog(uint8_t data) { portData = data; } +static void TraceLogAddValidData(uint8_t) {} +static unsigned millis() { return 0; } +static bool VMHostIO2(uint8_t, bool) { return false; } +static void VMHostPoll() {} +#define SetGameAssert (game = true) +#define SetGameDeassert (game = false) +#define SetExROMAssert (exrom = true) +#define SetExROMDeassert (exrom = false) +#define SetLEDOn (led = true) +#define SetLEDOff (led = false) +#define SetResetAssert (resetHeld = true) +#define SetDMADeassert (dmaReleased = true) +#define FILE_READ 0 +#define FLASHMEM +#define DMAMEM +#define FeatVMHost +#define Num8kSwapBuffers 2 +#define MaxNamePathLength 256 +#define MaxPathLength 128 +#define Printf_dbg(...) ((void)0) +#define Cart_Generic 0 +#define Cart_EasyFlash 32 +// Explicit address-model seam: hardware uses 32-bit addresses, native hosts +// may use 64. No production source is regex-rewritten or bank logic replaced. +#define uint32_t uintptr_t +#define SwapSeekAddrMask (uintptr_t{0xef} << (sizeof(uintptr_t) * 8 - 8)) +#ifndef TEST_MAIN_IMAGE +#define MinimumBuild +#endif +#include "MinimalBoot/Common/CrtChips.h" +void SendMsgPrintfln(const char*, ...); +uintptr_t toU32(uint8_t*); +uint16_t toU16(uint8_t*); +void FreeCrtChips(); +bool SetTypeFromCRT(StructMenuItem*, uint8_t, uint8_t); +bool ParseCRTHeader(StructMenuItem*, uint8_t*, uint8_t*); +static const struct { uint16_t HWID; uint8_t IOH; } HWID_IOH_Assoc[] = {{32, IOH_EasyFlash}, {79, IOH_MagicDesk2}, {85, IOH_MagicDesk2}}; +bool AssocHWID_IOH(uint16_t); + +#ifdef TEST_MAIN_IMAGE +static uint8_t* ptrRAM_ImageEnd; +uint8_t NumCrtChips; +StructCrtChip CrtChips[MAX_CRT_CHIPS]; +static bool LoadedEasyFlashROM; +static StructMenuItem* DriveDirMenu; +static uint8_t IO1[256]; +enum { rwRegNextIOHndlr, eepAdNextIOHndlr, eepAdCrtBootName, eepAdMinBootInd, MinBootInd_ExecuteMin }; +struct EEPROMMock { uint8_t read(unsigned) { return 42; } void write(unsigned, unsigned) {} } EEPROM; +static uintptr_t flash[0x4000 / sizeof(uintptr_t)]; +#define FLASH_BASEADDRESS uintptr_t(flash) +#define UpperAddr 0 +static void EEPwriteStr(unsigned, const char* path) { bootPath = path; } +static void RebootTR() { throw BootMinimal{}; } +static void FreeDriveDirMenu() { DriveDirMenu = nullptr; } +static void RedirectEmptyDriveDirMenu() {} +void SendMsgPrintfln(const char* format, ...) { char text[512]; va_list args; va_start(args, format); vsnprintf(text, sizeof text, format, args); va_end(args); Serial.println(text); } +uintptr_t toU32(uint8_t* p) { return uintptr_t(p[0]) << 24 | uintptr_t(p[1]) << 16 | uintptr_t(p[2]) << 8 | p[3]; } +uint16_t toU16(uint8_t* p) { return uint16_t(p[0]) << 8 | p[1]; } +bool AssocHWID_IOH(uint16_t type) { RegNextIOHndlr = type; return type == 32 || type == 79; } +#include "MinimalBoot/Common/C128Crt.h" +// A storage-only seam makes accidental 129th writes observable. The native +// MinimalBoot profile below uses its actual production declarations instead. +static struct { uintptr_t before; StructCrtChip chips[128]; uintptr_t after; } guarded{0xcafebabe, {}, 0xfacefeed}; +#define CrtChips guarded.chips +int16_t ParseChipHeader(uint8_t*, const char*); +bool LoadFile(FS*, const char*, StructMenuItem*); +#define malloc hostMalloc +#define free hostFree +#include "main-functions.h" +#undef malloc +#undef free +static StructMenuItem selection{"fixture.crt"}; +static bool load(FS* source = &SD) { reads = payloadAllocations = 0; messages.clear(); selection.ItemType = rtFileCrt; return LoadFile(source, "/", &selection); } +static void intact() { assert(guarded.before == 0xcafebabe && guarded.after == 0xfacefeed); } +#else +char DriveDirPath[] = "/"; +StructMenuItem DriveDirMenu{"fixture.crt"}; +bool PathIsRoot(); +bool LoadFile(StructMenuItem*, FS*); +int16_t ParseChipHeader(uint8_t*); +#ifdef VM_HOST_PROFILE +bool ParseLegacyChipHeader(uint8_t*); +#endif +#include "MinimalBoot/Common/IO_Handlers/IOH_EasyFlash.c" +#include "MinimalBoot/Common/IO_Handlers/IOH_MagicDesk2.c" +static stcIOHandlers* IOHandler[] = {nullptr, &IOHndlr_EasyFlash, &IOHndlr_MagicDesk2}; +#define malloc hostMalloc +#define calloc hostCalloc +#define free hostFree +#include "MinimalBoot/Min_DriveDirLoad.ino" +#undef malloc +#undef calloc +#undef free +static bool load() { reads = payloadAllocations = 0; messages.clear(); DriveDirMenu.ItemType = rtFileCrt; return LoadFile(&DriveDirMenu, &SD); } +#endif +#undef uint32_t + +static void be16(uint8_t* p, unsigned n) { p[0] = n >> 8; p[1] = n; } +static void be32(uint8_t* p, unsigned n) { p[0] = n >> 24; p[1] = n >> 16; p[2] = n >> 8; p[3] = n; } +static void header(unsigned type = 32) { fixture.assign(64, 0); memcpy(fixture.data(), "C64 CARTRIDGE ", 16); be32(fixture.data()+16, 64); be16(fixture.data()+20, 0x100); be16(fixture.data()+22, type); fixture[24] = type==32?1:0; } +static void chip(unsigned bank, unsigned half, unsigned size = 8192, unsigned type = 2, int fill = -1) { + const size_t at = fixture.size(); fixture.resize(at + 16 + size); uint8_t* p = fixture.data() + at; + memcpy(p, "CHIP", 4); be32(p+4, 16+size); be16(p+8, type); be16(p+10, bank); be16(p+12, half ? 0xa000 : 0x8000); be16(p+14, size); + for (unsigned i = 0; i < size; ++i) p[16+i] = fill < 0 ? bank*11 + half*0x59 + i*7 + (i>>8) : fill; +} +static void image(unsigned banks) { header(); for (unsigned b=0; b=16); + size_t bytes=std::min(size_t(65535),remaining-16); + if(remaining-16-bytes>0 && remaining-16-bytes<16) bytes-=16; + chip(65535,0,unsigned(bytes),1); + } +} +static std::vector expected(unsigned banks) { + std::vector result(banks*16384, 0xff); + for (size_t at=64; at& bytes, unsigned mask) { + portData=value; DMA_State=DMA_S_Idle; IO1Hndlr_EasyFlash(0,false); + if (DMA_State==DMA_S_StartImmediate) { ++swaps; DMA_State=DMA_S_ActiveReady; PollingHndlr_EasyFlash(); assert(DMA_State==DMA_S_StartDisable); } + assert(CurrentEasyFlashBank==(value&mask)); + assert(!memcmp(LOROM_Image, bytes.data()+(value&mask)*16384,8192)); + assert(!memcmp(HIROM_Image, bytes.data()+(value&mask)*16384+8192,8192)); +} +static void rejected(const char* reason) { + assert(!load()); if(messages.find(reason)==std::string::npos) fprintf(stderr,"Expected diagnostic '%s':\n%s",reason,messages.c_str()); + assert(messages.find(reason)!=std::string::npos); assert(!NumCrtChips && !CrtChips && !EasyFlashErasedBank && !myFile && allocations.empty()); +} +static void swapFailure(unsigned half, unsigned failure, bool startup=false) { + header(); chip(0,0); chip(0,1); chip(1,0); chip(1,1); chip(127,half); assert(load()); InitHndlr_EasyFlash(); + if (startup) { FreeCrtChips(); header(); chip(1,0); chip(1,1); chip(0,0); chip(0,1); assert(load()); } + else { portData=127; DMA_State=DMA_S_Idle; IO1Hndlr_EasyFlash(0,false); } + messages.clear(); doReset=false; resetHeld=dmaReleased=false; const unsigned before=returnsToMenu; + if (failure==0) { myFile.close(); failOpen=true; } else if (failure==1) failSeek=true; else shortRead=true; + try { if (startup) InitHndlr_EasyFlash(); else { DMA_State=DMA_S_ActiveReady; PollingHndlr_EasyFlash(); } assert(false); } + catch (ReturnToMenu&) {} + failOpen=failSeek=shortRead=false; + assert(returnsToMenu==before+1 && resetHeld && dmaReleased && !doReset && !LOROM_Image && !HIROM_Image && !game && !exrom); + assert(messages.find(failure==0?"open failed":failure==1?"seek failed":"truncated")!=std::string::npos); + assert(messages.find("returning to menu")!=std::string::npos); FreeCrtChips(); +} +#endif + +int main(int argc, char** argv) { +#ifdef TEST_MAIN_IMAGE + static_assert(MAX_CRT_CHIPS==128); payloadLimit=1024; + flash[0]=0x42464346; flash[0x1000/sizeof(uintptr_t)]=0x432000d1; flash[0x1000/sizeof(uintptr_t)+1]=uintptr_t(flash)+0x2000; + for (unsigned bank : {64u,127u,128u,255u}) { + header(); chip(0,0); chip(bank,1); bootPath.clear(); + try { load(); assert(false); } catch (BootMinimal&) { assert(bootPath=="/fixture.crt"); } + intact(); FreeCrtChips(); assert(allocations.empty()); + assert(!load(&USB)); assert(messages.find("from SD")!=std::string::npos); intact(); + } + header(); chip(0,0); chip(0,1); assert(load(&USB) && NumCrtChips==2); intact(); FreeCrtChips(); + header(); chip(65535,0,17,1); chip(0,0); chip(0,1); assert(load(&USB) && NumCrtChips==2); intact(); FreeCrtChips(); + header(79); for (unsigned b=0;b<128;++b) chip(b,0); assert(load(&USB) && NumCrtChips==128); intact(); FreeCrtChips(); + chip(128,0); assert(!load(&USB)); assert(messages.find("128")!=std::string::npos); intact(); + image(64); chip(63,1,8192,2,0xcc); assert(load(&USB) && NumCrtChips==128 && CrtChips[127].ChipROM[0]==0xcc); intact(); FreeCrtChips(); + image(64); chip(64,0); try { load(); assert(false); } catch (BootMinimal&) {} intact(); FreeCrtChips(); + puts("PASS main: static128 canaries, sparse high-bank SD handoff/USB refusal, low-bank USB, ignored types, duplicate reuse and routing before table limit"); +#elif defined(VM_HOST_PROFILE) + static_assert(MAX_CRT_CHIPS==128 && NumDecodeBanks==64); + for (unsigned b=0;b<64;++b) { BankDecode[b][0]=RAM_Image; BankDecode[b][1]=RAM_Image+8192; } + for (unsigned value=0;value<256;++value) { portData=value; IO1Hndlr_EasyFlash(0,false); assert(CurrentEasyFlashBank==(value&63) && LOROM_Image==RAM_Image && HIROM_Image==RAM_Image+8192); } + puts("PASS VM host: original static128 table, 64 decode banks and six-bit register wrapping"); +#else + static_assert(MAX_CRT_CHIPS==512); assert(argc>=3); + for (unsigned banks : {64u,128u,256u}) { + image(banks); auto bytes=expected(banks); assert(load() && NumCrtChips==banks*2); InitHndlr_EasyFlash(); + assert(EasyFlashBankMask==banks-1 && EasyFlashErasedBank); + for (unsigned value=0;value<256;++value) { select(value,bytes,banks-1); select(255-value,bytes,banks-1); } + for (unsigned value=0;value<256;++value) { portData=value; IO1Hndlr_EasyFlash(2,false); assert(game==bool(value&1)&&exrom==bool(value&2)&&led==bool(value&128)); portData=value^0xa5; IO2Hndlr_EasyFlash(value,false); } + for (unsigned value=0;value<256;++value) { IO2Hndlr_EasyFlash(value,true); assert(portData==(value^0xa5)); } + FreeCrtChips(); assert(allocations.empty()); + } + payloadLimit=0; + header(); for(unsigned b=1;b<256;++b) if(b%3==0) chip(b,b&1); chip(0,0); chip(0,1); chip(255,1); + auto bytes=expected(256); assert(load()); InitHndlr_EasyFlash(); for(unsigned b=0;b<256;++b) select(b,bytes,255); FreeCrtChips(); + image(256); chip(255,1,8192,2,0xcc); be16(fixture.data()+fixture.size()-8192-4,0xe000); chip(0,0,8192,0,0x77); + chip(65535,0,17,1); // ignored packets remain acceptable after all512 unique halves + bytes=expected(256); assert(load() && NumCrtChips==512); InitHndlr_EasyFlash(); select(255,bytes,255); select(0,bytes,255); FreeCrtChips(); + header(); chip(0,0); chip(0,1); be16(fixture.data()+64+8208+12,0xe000); chip(0,1,8192,0,0x55); + bytes=expected(64); assert(load() && NumCrtChips==2); InitHndlr_EasyFlash(); select(0,bytes,63); FreeCrtChips(); + header(); for(unsigned n=0;n<600;++n) chip(65535,0,17,1); chip(0,0); chip(0,1); assert(load() && NumCrtChips==2); InitHndlr_EasyFlash(); assert(EasyFlashBankMask==63); FreeCrtChips(); + header(); chip(65535,0,8192,1); rejected("No supported"); + // Duplicate/ignored streams may exceed the cartridge's logical capacity. + // Both new and replacement swapped payloads must fit the 24-bit file tag. + for(unsigned overlap : {0u,4096u}) { + header(); chip(0,0); chip(0,1); chip(1,0); + padIgnored(0x01000000u-overlap-16u); + chip(overlap?2:1,0); rejected("24-bit"); + } + header(); chip(0,0); chip(0,1); padIgnored(0x01000000u-8192u-16u); chip(2,1); + bytes=expected(64); assert(load()); InitHndlr_EasyFlash(); select(2,bytes,63); FreeCrtChips(); + image(64); be32(fixture.data()+68,0); assert(load()); FreeCrtChips(); + image(64); be32(fixture.data()+68,0xffffffff); assert(load()); FreeCrtChips(); + image(64); be16(fixture.data()+74,256); rejected("bank"); + image(64); be16(fixture.data()+76,0x9000); rejected("address"); + image(64); be16(fixture.data()+78,4096); rejected("4096"); + image(64); fixture[64]='!'; rejected("CHIP"); + image(64); fixture.pop_back(); rejected("Truncated"); + image(64); fixture.resize(fixture.size()-8192-8); rejected("Truncated"); + header(); chip(65535,0,17,1); chip(0,0); chip(0,1); fixture.resize(64+16+16); rejected("Too Short"); + image(64); chip(65535,0,17,1); fixture.pop_back(); rejected("Truncated"); + image(64); failMetadata=true; rejected("RAM"); failMetadata=false; + image(64); metadataBudget=MAX_CRT_CHIPS*sizeof(StructCrtChip); rejected("erased page"); metadataBudget=SIZE_MAX; + payloadLimit=2; image(64); failReadOn=7; rejected("Truncated"); failReadOn=0; + payloadLimit=0; + for(unsigned half=0;half<2;++half) for(unsigned failure=0;failure<3;++failure) swapFailure(half,failure); + for(unsigned failure=0;failure<3;++failure) swapFailure(0,failure,true); + header(79); for(unsigned b=0;b<128;++b) chip(b,0,16384,0); bytes=expected(128); + assert(load() && NumCrtChips==128 && !EasyFlashErasedBank); InitHndlr_MagicDesk2(); + for(unsigned value=0;value<256;++value) { + portData=value; DMA_State=DMA_S_Idle; IO1Hndlr_MagicDesk2(0,false); + if(DMA_State==DMA_S_StartImmediate) { DMA_State=DMA_S_ActiveReady; PollingHndlr_MagicDesk2(); } + assert(!memcmp(LOROM_Image,bytes.data()+(value&127)*16384,8192)); + assert(!memcmp(HIROM_Image,bytes.data()+(value&127)*16384+8192,8192)); + } + uint8_t fallback[8192]; const unsigned returned=returnsToMenu; shortRead=true; LoadBank(80,fallback); shortRead=false; + assert(returnsToMenu==returned && !memcmp(fallback,fixture.data()+80,4096)); + for(unsigned at=4096;at<8192;++at) assert(fallback[at]==0xff); + FreeCrtChips(); + chip(128,0,16384,0); rejected("128"); + header(0); memcpy(fixture.data(),"C128 CARTRIDGE",14); chip(0,0,16384,0); assert(load()); assert(DriveDirMenu.ItemType==rtBinC128); LaunchC128Cart(); assert(LOROM_Mask==0x3fff); FreeCrtChips(); + for(int index=1;index(input),{}); + unsigned highest=0; for(size_t at=64;at=128?256:highest>=64?128:64; bytes=expected(banks); assert(load()); InitHndlr_EasyFlash(); for(unsigned b=0;b<256;++b) select(b,bytes,banks-1); FreeCrtChips(); + printf("PASS cartridge readback: %u banks, complete ROML/ROMH bytes for all256 writes: %s\n",banks,argv[index]); + } + assert(allocations.empty() && swaps>1000); + printf("PASS MinimalBoot: direct production loader/EF/MD2 includes, all256 writes at1/2/4MiB, sparse/out-of-order halves, 512+duplicates, skipped types, diagnostics/menu recovery, allocation failures, legacy128 and C128; %u SD swaps\n",swaps); +#endif +} diff --git a/Source/Teensy/tests/easyflash.test.js b/Source/Teensy/tests/easyflash.test.js new file mode 100644 index 00000000..ef75ecd5 --- /dev/null +++ b/Source/Teensy/tests/easyflash.test.js @@ -0,0 +1,62 @@ +// 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 { blankComments } from '../../../tools/lib/source-text.mjs'; +import { makeEasyFlashFixture } from './easyflash-fixtures.mjs'; + +const root = path.resolve(import.meta.dirname, '..'); +// Main-image coverage is deliberately limited to routing and the static table +// boundary. Extract complete named functions, without editing their bodies. +function definition(source, name) { + const clean = blankComments(source); + const match = new RegExp(String.raw`^[^\n;{}]*\b${name}\s*\([^;{}]*\)\s*\{`, 'm').exec(clean); + assert(match, `Missing production function ${name}`); + const open = clean.indexOf('{', match.index); + let depth = 0; + for (let at = open; at < clean.length; ++at) { + if (clean[at] === '{') ++depth; + else if (clean[at] === '}' && --depth === 0) return source.slice(match.index, at + 1); + } + assert.fail(`Unbalanced production function ${name}`); +} + +test('EasyFlash native loader, routing and fixed VM profile regressions', (t) => { + const candidates = process.env.CXX ? [process.env.CXX] : ['g++', 'clang++', + ...(process.platform === 'win32' ? ['C:/msys64/mingw32/bin/g++.exe'] : [])]; + const compiler = candidates.find((command) => spawnSync(command, ['--version'], { windowsHide: true }).status === 0); + if (!compiler) return t.skip('Set CXX to g++ or clang++ to run the native firmware harness'); + const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'teensyrom-easyflash-')); + try { + const parser = fs.readFileSync(path.join(root, 'FileParsers.ino'), 'utf8'); + const loader = fs.readFileSync(path.join(root, 'DriveDirLoad.ino'), 'utf8'); + assert.match(blankComments(parser), /uint8_t\s+NumCrtChips\s*=\s*0\s*;/); + assert.match(blankComments(parser), /StructCrtChip\s+CrtChips\s*\[\s*MAX_CRT_CHIPS\s*\]\s*;/); + fs.writeFileSync(path.join(temporary, 'main-functions.h'), [ + ...['BootCrtFromSD', 'ParseCRTHeader', 'ParseChipHeader', 'SetTypeFromCRT'].map((name) => definition(parser, name)), + ...['LoadFile', 'FreeCrtChips'].map((name) => definition(loader, name)), + ].join('\n')); + const fixtures = [{mib: 2}, {mib: 4}, {mib: 2, sparse: true, reverse: true}, {mib: 4, sparse: true, reverse: true}].map((options) => { + const file = path.join(temporary, `bank-identifiers-${options.mib}m${options.sparse ? '-sparse-reversed' : ''}.crt`); + fs.writeFileSync(file, makeEasyFlashFixture(options)); return file; + }); + // Optional local readback only; cartridge bytes are never copied into Git. + fixtures.push(...JSON.parse(process.env.TEENSYROM_EASYFLASH_CRTS ?? '[]').map((file) => fs.realpathSync(file))); + const env = { ...process.env, PATH: path.dirname(compiler) + path.delimiter + process.env.PATH }; + for (const [profile, flags] of [['minimal', []], ['main', ['-DTEST_MAIN_IMAGE']], ['vm-host', ['-DVM_HOST_PROFILE']]]) { + const executable = path.join(temporary, profile + (process.platform === 'win32' ? '.exe' : '')); + const compiled = spawnSync(compiler, ['-std=c++17', '-O2', '-UNDEBUG', ...flags, + '-I', temporary, '-I', root, path.join(import.meta.dirname, 'easyflash-harness.cpp'), '-o', executable], + { encoding: 'utf8', windowsHide: true, env }); + assert.equal(compiled.status, 0, `${profile}: ${compiled.error ?? ''}\n${compiled.stdout}\n${compiled.stderr}`); + const result = spawnSync(executable, fixtures, { encoding: 'utf8', windowsHide: true, env }); + assert.equal(result.status, 0, `${profile}: ${result.error ?? ''}\n${result.stdout}\n${result.stderr}`); + assert.match(result.stdout, /PASS/); t.diagnostic(result.stdout.trim()); + } + } finally { + fs.rmSync(temporary, { recursive: true, force: true }); + } +}); diff --git a/docs/Architecture/Constraints.md b/docs/Architecture/Constraints.md index b5aff41a..9a055189 100644 --- a/docs/Architecture/Constraints.md +++ b/docs/Architecture/Constraints.md @@ -114,6 +114,19 @@ for why that specific fix is unsafe, not just inelegant. ## Memory budgets are hard caps, not soft targets +The main image keeps its static 128-entry CRT table in DTCM and its 64-bank +EasyFlash decoder. An EasyFlash bank of 64 or above takes the existing SD-only +MinimalBoot handoff even when a sparse file fits the main image's RAM. +Native MinimalBoot has a 256-bank decoder (2 KiB of RAM1); its CRT metadata is +allocated on the RAM2 heap, with at most 512 unique EasyFlash bank halves or +128 CHIP records for other cartridges. EasyFlash reserves its 8 KiB erased +`$FF` page in the same allocation before ROM payloads fill the heap. The page +must stay in RAM2 because the C64 bus ISR reads it directly; other cartridge +types allocate no erased page. Image buffers and the 16 swap buffers retain +their existing sizes. Large capacity comes from SD, not a larger resident ROM +buffer. `VM_HOST_PROFILE` keeps its static table, 64-bank client mapping and +original handler code; it carries no native EasyFlash metadata or erased page. + - Full firmware: `MaxRAM_ImageSize = 128` KB (`Source/Teensy/TeensyROM.h:26`) — the RAM1 image buffer. Beyond that, CRT banks spill into RAM2 via `malloc()` (`FileParsers.ino:120-128`); when that allocation fails, firmware reboots into MinimalBoot (`FileParsers.ino:165-168`, see [Teensy-Firmware.md](Teensy-Firmware.md#minimalboot-vs-full-firmware) for the full trigger mechanism) — this exhaustion point empirically lands around **~650KB** total, it is not a hardcoded threshold. - MinimalBoot: `(440 - 8*Num8kSwapBuffers - EthernetDeduction)` KB = 208 KB (`Source/Teensy/MinimalBoot/Min_TeensyROM.h:58`) — with RAM2 and the 16 swap buffers, holds **103 8K banks (~824KB)** before any bank has to come from SD at runtime (measured 2026-10-04: an 880K EasyFlash loads 87 banks, flags 23 for swapping, 16 of them preloaded into the swap buffers, 7 left for SD), at the cost of dropping USB host support (SD-only loading in this mode). Its RAM1 stack floor (>24000 free with Ethernet, measured in that header) is enforced by `tools/build-firmware.mjs`; the base was 392 until MinimalBoot's ITCM code dropped from five 32 KB banks to four. - Files beyond that use bank-swapping from SD (only), asserting the DMA line for about 0.5–1.5 mS per uncached swap depending on the card, 2.3 mS at worst so far (since 24c187c reads each 8K bank in one call; reading it a byte at a time took ~3 mS) — not true bus-mastering DMA, just the old-school REU-style pause assertion. TR+'s true bus-mastering DMA doesn't help here: the pause just needs to be perceptually instant to the 6510 for an SD→RAM bank transfer, which bus-mastering doesn't improve. This mechanism was documented as unreliable on most C128s and a low percentage of NTSC systems; **that reliability claim may now be stale and is due for a re-test** (flagged during architecture review, 2026-08-11 — see [Known-Issues.md](Known-Issues.md)). A DMA Pause check utility (`TODCheck`/Test+Diags BASIC tool) exists to test a given system before relying on this. diff --git a/docs/Architecture/Known-Issues.md b/docs/Architecture/Known-Issues.md index e22d083b..41e44dea 100644 --- a/docs/Architecture/Known-Issues.md +++ b/docs/Architecture/Known-Issues.md @@ -495,7 +495,7 @@ Three `USBHIDParser` instances (`hid1`, `hid2`, `hid3`) are declared for the USB Unlike the C64 side, where `Menu_Regs.i`/`Menu_Regs.h` at least document the sync requirement in a comment, these pairs carry no such marker — nothing signals that a fix in one file has a counterpart that likely needs the same fix. -**Already causing real drift, not just a theoretical risk:** MinimalBoot's `ParseChipHeader` correctly bounds-checks `NumCrtChips == MAX_CRT_CHIPS` before writing; the full-firmware `ParseChipHeader` in `DriveDirLoad.ino` does not. Same function, same name, same intended behavior, silently diverged. +**An example of real drift, now fixed:** MinimalBoot bounded its CHIP table before writing while the full-firmware `ParseChipHeader` in `FileParsers.ino` did not. Both now check their table capacity before creating a new entry. The main image retains its static 128-entry table; native MinimalBoot accepts up to 512 unique EasyFlash bank halves, with the legacy 128-entry limit for other cartridges. **No fix proposed** — restructuring this (shared translation units, a common library, etc.) would be a significant undertaking given the two builds' different memory/feature constraints, and isn't asked for. Recorded as a standing reminder: when fixing a bug in one of these known-duplicated functions, check whether the other build's copy needs the identical fix. diff --git a/docs/CRT_Implementation.md b/docs/CRT_Implementation.md index c1d1918b..dcccb050 100644 --- a/docs/CRT_Implementation.md +++ b/docs/CRT_Implementation.md @@ -14,6 +14,29 @@ * The Teensy's Flash memory and external add-on RAM locations are not quite fast enough to serve the C64. This is because they are serial devices and take about 400nS for a random read. We need about twice that fast to meet the 1MHz C64 bus timing. There are some discussions/attempts to increase speed documented [here](https://forum.pjrc.com/index.php?threads/faster-way-to-read-a-single-byte-from-flash-or-ext-psram.73428/) * That left us stuck at an 850KB CRT limit, until... +## Extended EasyFlash images + + * CRT hardware type 32 supports read-only images with 64, 128 or 256 banks (1, 2 or 4 MiB). + * Each accepted CHIP contains 8 KiB: ROML at `$8000` and ROMH at `$A000` or `$E000`. In MinimalBoot, omitted halves read as erased flash (`$FF`). + * MinimalBoot uses the highest accepted bank to select the `$DE00` mask: `$3F`, `$7F` or `$FF`. Ordinary 1 MiB images retain six-bit wrapping; the main image and extension host retain their 64-bank decoder. + * The main image keeps its static 128-entry CHIP table. Any EasyFlash bank of 64 or above uses MinimalBoot even in a small sparse file. MinimalBoot holds up to 512 unique bank halves on the RAM2 heap; other cartridge types retain their 128-CHIP limit. + * A later duplicate half replaces the earlier data without using another table slot. `$A000` and `$E000` address the same ROMH half. Packet-length fields are ignored, and CHIP types other than 0 and 2 are skipped using their declared ROM size. + * Accepted EasyFlash records require a bank below 256, an 8 KiB size and one of the addresses above. Short headers/payloads, failed reads and allocation failures produce a reason. Swapped payloads must fit the reader's 24-bit file-offset range, including their last byte; longer duplicate/skipped-packet streams are refused if they require an unrepresentable swap offset. A failed runtime SD bank read returns to the main menu. + * Extended images use the existing MinimalBoot SD bank-swap mechanism below. Launch them from SD; USB launches that need MinimalBoot are refused with a message. No host extension or external PSRAM is needed. EAPI writes remain unsupported. + +### Loader regression fixtures + +`node --test Source/Teensy/tests/easyflash.test.js` compiles the production MinimalBoot loader and handlers against native SD/register stand-ins. Focused main-image checks cover the static table boundary and SD-only high-bank handoff. + +Generate original bank-identification data without game assets: + +``` +node Source/Teensy/tests/easyflash-fixtures.mjs --mib 2 --output ef2.crt +node Source/Teensy/tests/easyflash-fixtures.mjs --mib 4 --sparse --reverse --output ef4-sparse-reverse.crt +``` + +The generator refuses to overwrite a file. These are loader fixtures, not runnable C64 applications. Each half starts with its bank number, half number and bank XOR `$5A`, followed by a deterministic pattern and a readable bank label. `--sparse` omits selected halves; `--reverse` puts bank zero last. + ## Very Large (>850KB) CRT file support: * Beta starting in FW v0.6.7 * Example files in this category "A Pig Quest", "Eye of the Beholder" and "SNK vs Capcom" diff --git a/docs/General_Usage.md b/docs/General_Usage.md index 6a7ff1af..06e22754 100644 --- a/docs/General_Usage.md +++ b/docs/General_Usage.md @@ -102,6 +102,7 @@ * See full CRT implementation details [here](CRT_Implementation.md). * On rev 0.2x PCBs, when using the C128 to emulate "Ultimax" carts (Deadtest, Jupiter Lander), some screen artifacts are visible. * This issue only impacts UltiMax CRTs on C128s and is resolved in PCB rev 0.3 + * EasyFlash supports read-only 1, 2 and 4 MiB images (64, 128 or 256 banks). Larger images use the SD-card bank-swap path described above. * EasyFlash EAPI not currently supported * **.SID files:** * Play SID file: ~90% of known SID files are supported