Login

Subversion Repositories NedoOS

Rev

Blame | Last modification | View Log | Download | RSS feed

/*

  SjASMPlus Z80 Cross Compiler - modified - SAVECPCSNA extension

  Copyright (c) 2006 Sjoerd Mastijn (original SW)

  This software is provided 'as-is', without any express or implied warranty.
  In no event will the authors be held liable for any damages arising from the
  use of this software.

  Permission is granted to anyone to use this software for any purpose,
  including commercial applications, and to alter it and redistribute it freely,
  subject to the following restrictions:

  1. The origin of this software must not be misrepresented; you must not claim
         that you wrote the original software. If you use this software in a product,
         an acknowledgment in the product documentation would be appreciated but is
         not required.

  2. Altered source versions must be plainly marked as such, and must not be
         misrepresented as being the original software.

  3. This notice may not be removed or altered from any source distribution.

*/


// io_cpc.cpp

#include "sjdefs.h"
#include "io_cpc_ldrs.h"

//
// Amstrad CPC snapshot file saving (SNA)
//

namespace
{
        // report error and close the file
        static int writeError(const char* fname, FILE*& fileToClose) {
                Error("[SAVECPCSNA] Write error (disk full?)", fname, IF_FIRST);
                fclose(fileToClose);
                return 0;
        }

        static bool isCPC6128() {
                return strcmp(DeviceID, "AMSTRADCPC464");
        }

        static word getCPCMemoryDepth() {
                return Device->PagesCount * 0x10;
        }
}

static int SaveSNA_CPC(const char* fname, word start) {
        // for Lua
        if (!DeviceID) {
                Error("SAVECPCSNA only allowed in real device emulation mode (See DEVICE)"); return 0;
        }
        else if (!IsAmstradCPCDevice(DeviceID)) {
                Error("[SAVECPCSNA] Device must be AMSTRADCPC464 or AMSTRADCPC6128."); return 0;
        }

        FILE* ff;
        if (!FOPEN_ISOK(ff, fname, "wb")) {
                Error("[SAVECPCSNA] Error opening file for write", fname);
                return 0;
        }

        // format:  http://cpctech.cpc-live.com/docs/snapshot.html
        constexpr int SNA_HEADER_SIZE = 256;
        const char magic[8] = { 'M', 'V', ' ', '-', ' ', 'S', 'N', 'A' };
        // basic rom initialized pens
        const byte ga_pens[17] = { 0x04, 0x0A, 0x13, 0x0C, 0x0B, 0x14, 0x15, 0x0D, 0x06, 0x1E, 0x1F, 0x07, 0x12, 0x19, 0x04, 0x17, 0x04 };
        // crtc set to standard mode 1 screen @ $C000
        const byte crtc_defaults[18] = {
                0x3F,           // h total
                0x28,           // h displayed
                0x2E,           // h sync pos
                0x8E,           // h/v sync widths
                0x26,           // v total (height)
                0x00,           // v adjust
                0x19,           // v displayed (height)
                0x1E,           // v sync pos
                0x00,           // interlace & skew
                0x07,           // max raster
                0x00, 0x00, // cursor start
                0x30, 0x00, // display (xxPPSSOO) -> 0xC000 based screen
                0x00, 0x00, // cursor addr
                0x00, 0x00  // light pen
        };
        // psg defaults as initialized by ROM
        const byte psg_defaults[16] = { 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };

        // init header
        byte snbuf[SNA_HEADER_SIZE];
        memset(snbuf, 0, SNA_HEADER_SIZE);
        // copy over the magic marker
        memcpy(snbuf, magic, sizeof(magic));
        snbuf[0x10] = 2; // v2 file format

        // v1 format fields
        snbuf[0x1B] = 0; snbuf[0x1C] = 0; // ensure interrupts are disabled
        snbuf[0x21] = 0xF0; snbuf[0x22] = 0xBF; // set the sp to $BFF0
        snbuf[0x23] = start & 0xFF; snbuf[0x24] = start >> 8; // pc set to start addr
        snbuf[0x25] = 1; // im = 1
        // set the pens to the defaults
        memcpy(snbuf + 0x2F, ga_pens, sizeof(ga_pens));
        // multi-config (RMR: 100I ULVM)
        snbuf[0x40] = 0b1000'01'01; // Upper ROM paged out, Lower ROM paged in, Mode 1
        // RAM config (MMR see https://www.grimware.org/doku.php/documentations/devices/gatearray#mmr)
        snbuf[0x41] = 0; // default RAM paging
        // set the crtc registers to the default values
        snbuf[0x42] = 0x0D;     // selected crtc reg
        memcpy(snbuf + 0x43, crtc_defaults, sizeof(crtc_defaults));
        // PPI
        snbuf[0x59] = 0x82;     // PPI control port default
        // Set the PSG registers to sensible defaults
        memcpy(snbuf + 0x5B, psg_defaults, sizeof(psg_defaults));

        word memdepth = getCPCMemoryDepth();
        snbuf[0x6B] = memdepth & 0xFF;
        snbuf[0x6C] = memdepth >> 8;

        // v2 format fields
        snbuf[0x6D] = isCPC6128() ? 2 : 0;      // machine type (0 = 464, 1 = 664, 2 = 6128)

        if (fwrite(snbuf, 1, SNA_HEADER_SIZE, ff) != SNA_HEADER_SIZE) {
                return writeError(fname, ff);
        }

        // Write the pages out in order
        for (int page = 0; page < Device->PagesCount; ++page) {
                if ((aint)fwrite(Device->GetPage(page)->RAM, 1, Device->GetPage(page)->Size, ff) != Device->GetPage(page)->Size) {
                        return writeError(fname, ff);
                }
        }

        fclose(ff);
        return 1;
}

void dirSAVECPCSNA() {
        if (pass != LASTPASS) {
                SkipToEol(lp);
                return;
        }
        std::unique_ptr<char[]> fnaam(GetOutputFileName(lp));
        int start = StartAddress;
        if (anyComma(lp)) {
                aint val;
                if (ParseExpression(lp, val)) {
                        if (0 <= start) Warning("[SAVECPCSNA] Start address was also defined by END, SAVECPCSNA argument used instead");
                        if (0 <= val) {
                                start = val;
                        }
                        else {
                                Error("[SAVECPCSNA] Negative values are not allowed", bp, SUPPRESS); return;
                        }
                }
                else {
                        return;
                }
        }
        if (start < 0) {
                Error("[SAVECPCSNA] No start address defined", bp, SUPPRESS); return;
        }

        if (!SaveSNA_CPC(fnaam.get(), start))
                Error("[SAVECPCSNA] Error writing file (Disk full?)", bp, IF_FIRST);
}

//
// Amstrad CPC tape file saving (CDT)
//

enum ECDTHeadlessFormat { AMSTRAD, SPECTRUM };

namespace CDTUtil {

        static constexpr word DefaultPause = 1000;
//      static constexpr byte BlockTypeReg = 0x0A;                      //unused currently, TODO ask Oli about it?
        static constexpr byte BlockTypeHeader = 0x2C;
        static constexpr byte BlockTypeData = 0x16;

        static constexpr byte FileTypeBASIC = (0 << 1);
        static constexpr byte FileTypeBINARY = (1 << 1);
//      static constexpr byte FileTypeSCREEN = (2 << 1);        //unused currently, TODO ask Oli about it?

        /* CRC polynomial: X^16+X^12+X^5+1 */
        static unsigned int crcupdate(word c, byte b) {
                constexpr unsigned int poly = 4129;
                unsigned int aux = c ^ (b << 8);
                for (aint i = 0; i < 8; i++) {
                        if (aux & 0x8000) {
                                aux = (aux << 1) ^ poly;
                        }
                        else {
                                aux <<= 1;
                        }
                }
                return aux;
        }

        static void writeChunkedData(const char* fname, const byte* buf, const aint buflen, word pauseAfter, byte sync) {
                constexpr aint chunkLen = 256;

                const aint chunkCount = (buflen + 255) >> 8;
                const aint dataLen = (chunkCount * chunkLen)    // data size (in chunks)
                        + (chunkCount * 2)      // crcs
                        + 5; // sync byte + trailer

                std::unique_ptr<byte[]> chunkedData(new byte[dataLen]);
                byte* wptr = chunkedData.get(); // write ptr
                const byte* rptr = buf; // read ptr
                // build the buffer
                *(wptr++) = sync; // sync pattern

                aint remaining = buflen;
       
                // N chunks of 256 bytes with a 2 byte checksum at the end
                for (aint i = 0; i < chunkCount; ++i) {        
                        if (remaining < chunkLen) {
                                memcpy(wptr, rptr, remaining);
                                memset(wptr + remaining, 0, chunkLen - remaining);
                                rptr += remaining;
                                remaining = 0;
                        }
                        else {
                                memcpy(wptr, rptr, chunkLen);
                                rptr += chunkLen;
                                remaining -= chunkLen;
                        }

                        unsigned int check = 0xFFFF;
                        for (aint n = 0; n < chunkLen; ++n) {
                                check = crcupdate(check, wptr[n]);
                        }

                        wptr += chunkLen;
                        // append block crc
                        check ^= 0xFFFF;
                        *(wptr++) = check >> 8;
                        *(wptr++) = check & 0xFF;
                }

                // 4 trailer bytes of 0xFF
                const byte trailer[] = { 0xFF, 0xFF, 0xFF, 0xFF };
                memcpy(wptr, trailer, sizeof(trailer));

                // save block
                STZXTurboBlock turbo;
                turbo.PilotPulseLen = 0x091A;
                turbo.FirstSyncLen = 0x048D;
                turbo.SecondSyncLen = 0x048D;
                turbo.ZeroBitLen = 0x048D;
                turbo.OneBitLen = 0x091A;
                turbo.PilotToneLen = 0x1000;
                turbo.LastByteUsedBits = 0x08;
                turbo.PauseAfterMs = pauseAfter;

                TZX_AppendTurboBlock(fname, chunkedData.get(), dataLen, turbo);
        }

        static void writeTapeFile(const char* fname, const char* tfname, byte fileType, const byte* buf, aint buflen, word memaddr, word startaddr, word pause) {
                constexpr aint blocksize = 2048;
                constexpr aint headerlen = 64;

                byte hbuf[headerlen];
                memset(hbuf, 0, headerlen);
                /*
                   0   16  Filename       Name of the file, padded with nulls
                  16    1  Block number   The first block is 1, numbers are consecutive
                  17    1  Last block     A non-zero value means that this is the last block of a file
                  18    1  File type      A value recording  the type of the file
                  19    2  Data length    The number of data bytes in the data record
                  21    2  Data location  Where the data was written from originally
                  23    1  First block    A non-zero value means that this is the first block of a file
                  24    2  Logical length This is the total length of the file in bytes
                  26    2  Entry address  The execution address for machine code programs
                */

               
                // ensure name is <= 16
                aint tapefname_len = strlen(tfname);
                if (tapefname_len > 16) {
                        tapefname_len = 16;
                }

                // copy tape file name (16 bytes)
                memcpy(hbuf, tfname, tapefname_len);

                // init header
                hbuf[16] = 1; // block 1
                hbuf[18] = fileType;
                hbuf[24] = buflen & 0xFF;       // logical len (size of whole file)
                hbuf[25] = buflen >> 8;
                hbuf[26] = startaddr & 0xFF;
                hbuf[27] = startaddr >> 8; // entry addr

                word memloc = memaddr;
                aint remaining = buflen;
                byte block = 1;
                const byte* rptr = buf;

                // split the file into blocks of up to 2048 bytes, each with a header and a payload
                while (remaining) {
                        hbuf[16] = block;       // write block num
                        hbuf[21] = memloc & 0xFF; // where's this block going in memory?
                        hbuf[22] = memloc >> 8;
                        hbuf[23] = block == 1 ? 0xFF : 0x00;    // first block flag

                        aint dlen = remaining;
                        if (remaining > blocksize) {
                                dlen = blocksize;
                                hbuf[17] = 0x00;        // more blocks to come
                        }
                        else {
                                hbuf[17] = 0xFF;        // last block
                        }

                        // write data len (size of block)
                        hbuf[19] = dlen & 0xFF;
                        hbuf[20] = dlen >> 8;

                        writeChunkedData(fname, hbuf, headerlen, 10, BlockTypeHeader); // header
                        writeChunkedData(fname, rptr, dlen, pause, BlockTypeData);      // data
                        rptr += dlen;
                        memloc += dlen;
                        remaining -= dlen;
                        ++block;
                }
        }

        static void writeBASICLoader(const char* fname, byte screenMode, const byte* palette) {
                constexpr byte mode_values[] = { 0x0E, 0x0F, 0x10 };
                byte border = 0;
                border = *palette;
                // Border is always the first entry in the palette
                ++palette;

                // BASIC number encoding format for the palette entries
                byte p[32];
                for (aint i = 0, n = 0; i < 16; ++i, n+=2) {
                        p[n] = (palette[i] / 10) + '0';
                        p[n+1] = (palette[i] % 10) + '0';
                }

                const word callad = SaveCDT_AmstradCPC464_ORG;
                const word himem = callad - 1; // himem is one byte lower than the program we load

                // BASIC loader to set the screen mode, border color, palette, and then load the asm loader and execute it
                const byte basic[] = {
                        // Line format <Line Len (int16)> <line no (int16)> <tokens...> <0x00>
                        //
                        //              10              MODE            N                                               :               CLS  :     BORDER               N
                        15, 00, 10, 00, 0xAD, 0x20, mode_values[screenMode], 0x01, 0x8A, 0x01, 0x82, 0x20, 0x19, border, 0x00,
                        //              20              DATA [16 bytes of int8]
                        54, 00, 20, 00, 0x8C, 0x20,
                                p[0], p[1], ',', p[2], p[3], ',', p[4], p[5], ',', p[6], p[7], ',',
                                p[8], p[9], ',', p[10], p[11], ',', p[12], p[13], ',', p[14], p[15], ',',
                                p[16], p[17], ',', p[18], p[19], ',', p[20], p[21], ',', p[22], p[23], ',',
                                p[24], p[25], ',', p[26], p[27], ',', p[28], p[29], ',', p[30], p[31], 0x00,
                        //              30              FOR                     i                                               =         0                       TO              15
                        18, 00, 30, 00, 0x9E, 0x20, 0x0D, 0x00, 0x00, 0xE9, 0xEF, 0x0E, 0x20, 0xEC, 0x20, 0x19, 0x0F, 0x00,
                        //              40              READ            v                                               :         INK                   i                                         ,           v                       ,                             v
                        30, 00, 40, 00, 0xC3, 0x20, 0x0D, 0x00, 0x00, 0xF6, 0x01, 0xA2, 0x20, 0x0D, 0x00, 0x00, 0xE9, 0x2C, 0x20, 0x0D, 0x00, 0x00, 0xF6, 0x2C, 0x20, 0x0D, 0x00, 0x00, 0xF6, 0x00,
                        //              50              NEXT            i
                        11, 00, 50, 00, 0xB0, 0x20, 0x0D, 0x00, 0x00, 0xE9, 0x00,
                        //              60              MEMORY          &NNNN                     :             LOAD
                        20, 00, 60, 00, 0xAA, 0x20, 0x1C, himem & 0xFF, himem >> 8, 0x01,       0xA8, 0x20, 0x22, '!', 'c', 'o', 'd', 'e', 0x22, 0x00,
                        //              70              CALL
                        10, 00, 70, 00, 0x83, 0x20, 0x1C, callad & 0xFF, callad >> 8, 0x00,
                        // EOF
                        00, 00
                };
                constexpr aint basiclen = sizeof(basic);
                writeTapeFile(fname, "LOADER", FileTypeBASIC, basic, basiclen, 0x0170, 0x0000, DefaultPause);
        }

        static void writeUserProgram(const char* fname, const char* tapefname, const byte* buf, aint buflen, word baseAddr, word startAddr) {
                writeTapeFile(fname, tapefname, FileTypeBINARY, buf, buflen, baseAddr, startAddr, DefaultPause);
        }

        static bool hasScreen() {
                const CDevicePage* page = Device->GetPage(3);
                const byte* ptr = page->RAM;
                for (int i = 0; i < page->Size; ++i) {
                        if (*ptr != 0) {
                                return true;
                        }
                        ++ptr;
                }
                return false;
        }

        static aint calcRAMStart(const byte* ram, aint ramlen) {
                for (int i = 0; i < ramlen; ++i) {
                        if (ram[i]) return i;
                }
                return 0x10000;
        }

        static aint calcRAMLength(const byte* ram, aint ramlen) {
                while (ramlen--) {
                        if (ram[ramlen]) return 1 + ramlen;
                }
                return 0x0000;
        }

        static std::unique_ptr<byte[]> getContigRAM(aint startAddr, aint length) {
                assert(0 <= startAddr && 1 <= length && (startAddr+length) <= 0x10000);
                std::unique_ptr<byte[]> data(new byte[length]);
                byte* bptr = data.get();
                // copy the currently mapped device memory into new continuous buffer
                while (0 < length) {
                        const int slotId = Device->GetSlotOfA16(startAddr);
                        assert(-1 != slotId);
                        CDeviceSlot* const S = Device->GetSlot(slotId);
                        const aint offset = startAddr - S->Address;
                        const aint slotLength = std::min(S->Size - offset, length);
                        memcpy(bptr, S->Page->RAM+offset, slotLength);
                        bptr += slotLength;
                        startAddr += slotLength;
                        length -= slotLength;
                }
                assert(0 == length);
                return data;
        }

/* //unused currently, TODO ask Oli about it?
        static constexpr byte basicToHWColor[] = {
                0x54, 0x44, 0x55, 0x5C, 0x58, 0x5D,     0x4C, 0x45,
                0x4D, 0x56,     0x46, 0x57,     0x5E, 0x40,     0x5F, 0x4E,
                0x47, 0x4F,     0x52, 0x42,     0x53, 0x5A,     0x59, 0x5B,
                0x4A, 0x43,     0x4B, 0x41,     0x48, 0x49,     0x50, 0x51,
        };
*/

}

static void createCDTDump464(const char* fname, aint startAddr, byte screenMode, const byte* palette) {

        byte* ramptr;
        aint ram_size = 0xC000; // 3 x 16K pages (eg: excl screen)
        std::unique_ptr<byte[]> ram(new byte[ram_size]);
        ramptr = ram.get();
        memcpy(ramptr + 0x0000, Device->GetPage(0)->RAM, 0x4000);
        memcpy(ramptr + 0x4000, Device->GetPage(1)->RAM, 0x4000);
        memcpy(ramptr + 0x8000, Device->GetPage(2)->RAM, 0x4000);

        if (screenMode > 2) {
                screenMode = 0xFF; // Turn mode & palette select off
        }
        bool hasScreen = CDTUtil::hasScreen();
        aint ramBase = CDTUtil::calcRAMStart(ramptr, ram_size);
        aint ramEnd = CDTUtil::calcRAMLength(ramptr, ram_size);

        if (0x10000 == ramBase || 0x0000 == ramEnd) {
                Error("[SAVECDT] Could not determine the start and end of the program", nullptr, SUPPRESS); return;
        }

        aint ramUsed = ramEnd - ramBase;

        if (startAddr < 0) {
                startAddr = ramBase;
        }

        // construct the asm loader
        byte loader[SaveCDT_AmstradCPC464_Len];
        memcpy(loader, SaveCDT_AmstradCPC464, SaveCDT_AmstradCPC464_Len);

        // loader settings
        loader[SaveCDT_AmstradCPC464_Settings + 0x0] = hasScreen;
        loader[SaveCDT_AmstradCPC464_Settings + 0x1] = startAddr & 0xFF;
        loader[SaveCDT_AmstradCPC464_Settings + 0x2] = startAddr >> 8;

        loader[SaveCDT_AmstradCPC464_Settings + 0x3] = ramBase & 0xFF;
        loader[SaveCDT_AmstradCPC464_Settings + 0x4] = ramBase >> 8;

        loader[SaveCDT_AmstradCPC464_Settings + 0x5] = ramUsed & 0xFF;
        loader[SaveCDT_AmstradCPC464_Settings + 0x6] = ramUsed >> 8;

        // Create an empty file with a 2s pause to start with
        TZX_CreateEmpty(fname);
        TZX_AppendPauseBlock(fname, 2000);

        // append a CPC basic loader which will run the asm loader
        CDTUtil::writeBASICLoader(fname, screenMode, palette);

        // append the asm loader program
        CDTUtil::writeUserProgram(fname, "CODE", loader, SaveCDT_AmstradCPC464_Len, SaveCDT_AmstradCPC464_ORG, SaveCDT_AmstradCPC464_ORG);

        // append screen if we have one
        if (hasScreen) {
                CDTUtil::writeChunkedData(fname, Device->GetPage(3)->RAM, Device->GetPage(3)->Size, CDTUtil::DefaultPause, CDTUtil::BlockTypeData);
        }
        // finally write the main code
        CDTUtil::writeChunkedData(fname, ramptr + ramBase, ramUsed, CDTUtil::DefaultPause, CDTUtil::BlockTypeData);
}

static void createCDTDump6128(const char* fname, aint startAddr, byte screenMode, const byte* palette) {

        byte* ramptr;
        aint ram_size = 0xC000; // 3 x 16K pages (eg: excl screen)
        std::unique_ptr<byte[]> ram(new byte[ram_size]);
        ramptr = ram.get();
        memcpy(ramptr + 0x0000, Device->GetPage(0)->RAM, 0x4000);
        memcpy(ramptr + 0x4000, Device->GetPage(1)->RAM, 0x4000);
        memcpy(ramptr + 0x8000, Device->GetPage(2)->RAM, 0x4000);

        if (screenMode > 2) {
                screenMode = 0xFF; // Turn mode & palette select off
        }
        bool hasScreen = CDTUtil::hasScreen();
        aint ramBase = CDTUtil::calcRAMStart(ramptr, ram_size);
        aint ramEnd = CDTUtil::calcRAMLength(ramptr, ram_size);

        if (0x10000 == ramBase || 0x0000 == ramEnd) {
                Error("[SAVECDT] Could not determine the start and end of the program", nullptr, SUPPRESS); return;
        }

        aint ramUsed = ramEnd - ramBase;

        if (startAddr < 0) {
                startAddr = ramBase;
        }

        // the max possible length for the loader
        constexpr aint max_loader_len = SaveCDT_AmstradCPC6128_Len + (SaveCDT_AmstradCPC6128_PageEntrySize * 4);
        // construct the asm loader
        byte loader[max_loader_len];
        memcpy(loader, SaveCDT_AmstradCPC6128, SaveCDT_AmstradCPC6128_Len);
        aint loader_actual_len = SaveCDT_AmstradCPC6128_Len;

        // loader settings
        loader[SaveCDT_AmstradCPC6128_Settings + 0x0] = hasScreen;
        loader[SaveCDT_AmstradCPC6128_Settings + 0x1] = startAddr & 0xFF;
        loader[SaveCDT_AmstradCPC6128_Settings + 0x2] = startAddr >> 8;

        loader[SaveCDT_AmstradCPC6128_Settings + 0x3] = ramBase & 0xFF;
        loader[SaveCDT_AmstradCPC6128_Settings + 0x4] = ramBase >> 8;

        loader[SaveCDT_AmstradCPC6128_Settings + 0x5] = ramUsed & 0xFF;
        loader[SaveCDT_AmstradCPC6128_Settings + 0x6] = ramUsed >> 8;

        byte* loader_pages = loader + SaveCDT_AmstradCPC6128_Pages;
        byte* loader_entries = loader_pages + 1;

        unsigned char* pages_ram[4];
        aint pages_len[4];
        aint pages_start[4];
        aint count = 0;

        // Ignore the lower 64K at this time!
        for (aint i = 4; i < Device->PagesCount; i++) {
                {
                        // Calc start and end of this block
                        aint base = CDTUtil::calcRAMStart(Device->GetPage(i)->RAM, 0x4000);
                        aint length = CDTUtil::calcRAMLength(Device->GetPage(i)->RAM, 0x4000);

                        if (0x10000 == base || 0 == length) {
                                continue;
                        }

                        loader_entries[0] = i;  // configuration (4-7)
                        loader_entries[1] = base & 0xFF;
                        loader_entries[2] = (base >> 8) & 0xFF;
                        loader_entries[3] = length & 0xFF;
                        loader_entries[4] = (length >> 8) & 0xFF;

                        loader_actual_len += SaveCDT_AmstradCPC6128_PageEntrySize;
                        loader_entries += SaveCDT_AmstradCPC6128_PageEntrySize;

                        pages_ram[count] = Device->GetPage(i)->RAM;
                        pages_start[count] = base;
                        pages_len[count] = length;

                        ++count;
                }
        }

        loader_pages[0] = count;

        // Create an empty file with a 2s pause to start with
        TZX_CreateEmpty(fname);
        TZX_AppendPauseBlock(fname, 2000);

        // append a CPC basic loader which will run the asm loader
        CDTUtil::writeBASICLoader(fname, screenMode, palette);

        // append the asm loader program
        CDTUtil::writeUserProgram(fname, "CODE", loader, loader_actual_len, SaveCDT_AmstradCPC6128_ORG, SaveCDT_AmstradCPC6128_ORG);

        // append screen if we have one
        if (hasScreen) {
                CDTUtil::writeChunkedData(fname, Device->GetPage(3)->RAM, Device->GetPage(3)->Size, CDTUtil::DefaultPause, CDTUtil::BlockTypeData);
        }

        // Write each of the pages
        for (aint i = 0; i < count; ++i) {
                CDTUtil::writeChunkedData(fname, pages_ram[i] + pages_start[i], pages_len[i], CDTUtil::DefaultPause, CDTUtil::BlockTypeData);
        }

        // Now drop the rest of the low 64k
        CDTUtil::writeChunkedData(fname, ramptr + ramBase, ramUsed, CDTUtil::DefaultPause, CDTUtil::BlockTypeData);
}

static void SaveCDT_SnapshotWithPalette(const char* fname, aint startAddr, byte screenMode, const byte* palette) {
        isCPC6128() ?
                createCDTDump6128(fname, startAddr, screenMode, palette) :
                createCDTDump464(fname, startAddr, screenMode, palette);
}

static void SaveCDT_Snapshot(const char* fname, aint startAddr) {
        // Default mode after loading from BASIC
        constexpr byte mode = 1;
        // Default ROM palette
        constexpr byte palette[] = {
                 1,
                 1, 24, 20,  6, 26,  0,  2,  8,
                10, 12, 14, 16, 18, 22, 24, 16,
        };
        SaveCDT_SnapshotWithPalette(fname, startAddr, mode, palette);
}

static void SaveCDT_BASIC(const char* fname, const char* tfname, aint startAddr, aint length) {
        std::unique_ptr<byte[]> data(CDTUtil::getContigRAM(startAddr, length));

        CDTUtil::writeTapeFile(fname, tfname, CDTUtil::FileTypeBASIC, data.get(), length, startAddr, 0x0000, CDTUtil::DefaultPause);
}

static void SaveCDT_Code(const char* fname, const char* tfname, aint startAddr, aint length, aint entryAddr) {
        if (entryAddr < 0) entryAddr = startAddr;

        std::unique_ptr<byte[]> data(CDTUtil::getContigRAM(startAddr, length));
        CDTUtil::writeTapeFile(fname, tfname, CDTUtil::FileTypeBINARY, data.get(), length, startAddr, entryAddr, CDTUtil::DefaultPause);
}

static void SaveCDT_Headless(const char* fname, aint startAddr, aint length, byte sync, ECDTHeadlessFormat format) {
        assert(ECDTHeadlessFormat::AMSTRAD == format || ECDTHeadlessFormat::SPECTRUM == format);
        std::unique_ptr<byte[]> data(CDTUtil::getContigRAM(startAddr, length));

        if (format == ECDTHeadlessFormat::AMSTRAD) CDTUtil::writeChunkedData(fname, data.get(), length, CDTUtil::DefaultPause, sync);
        else TZX_AppendStandardBlock(fname, data.get(), length, CDTUtil::DefaultPause, sync);
}

typedef void (*savecdt_command_t)(const char*);

// Creates a CDT tape file of a full memory snapshot, with loader
static void dirSAVECDTFull(const char* cdtname) {
        constexpr const char* argerr = "[SAVECDT] Invalid args. SAVECDT FULL <cdtname>[,<startaddr>[,<screenmode>[,<border>[,<ink0>...<ink15>]]]]";

        aint args[] = {
                StartAddress,
                0xFF, 0, // mode, border
                0, 0, 0, 0, 0, 0, 0, 0, // palette
                0, 0, 0, 0, 0, 0, 0, 0,
        };

        bool opt[] = {
                false// this is used only when comma was parsed after cdtname => not optional then
                true, true,
                true, true, true, true, true, true, true, true,
                true, true, true, true, true, true, true, true,
        };

        if (anyComma(lp) && !getIntArguments<19>(lp, args, opt)) {
                Error(argerr, lp, SUPPRESS); return;
        }

        if (args[1] != 0xFF) {
                byte palette[17];
                for (aint i = 0; i < 17; ++i) {
                        palette[i] = args[2 + i];
                }

                SaveCDT_SnapshotWithPalette(cdtname, args[0], args[1], palette);
        }
        else {
                SaveCDT_Snapshot(cdtname, args[0]);
        }
}

static void dirSAVECDTEmpty(const char* cdtname) {
        // EMPTY <cdtname>
        TZX_CreateEmpty(cdtname);
}

static void dirSAVECDTBasic(const char* cdtname) {
        constexpr const char* argerr = "[SAVECDT] Invalid args. SAVECDT BASIC <cdtname>,<name>,<start>,<length>";

        if (!anyComma(lp)) {
                Error(argerr, lp, SUPPRESS); return;
        }

        std::unique_ptr<char[]> tfname(GetFileName(lp));       
        if (!anyComma(lp)) {
                Error(argerr, lp, SUPPRESS); return;
        }

        aint args[] = { /*0:start*/ 0, /*1:length*/ 0 };
        bool opt[] = { false, false };
        if (!getIntArguments<2>(lp, args, opt) || args[0] < 0 || args[1] < 1 || 0x10000 <= args[1] || 0x10000 < (args[0]+args[1])) {
                Error(argerr, lp, SUPPRESS); return;
        }

        SaveCDT_BASIC(cdtname, tfname.get(), args[0], args[1]);
}

static void dirSAVECDTCode(const char* cdtname) {
        constexpr const char* argerr = "[SAVECDT] Invalid args. SAVECDT CODE <cdtname>,<name>,<start>,<length>[,<customstartaddress>]";

        if (!anyComma(lp)) {
                Error(argerr, lp, SUPPRESS); return;
        }

        std::unique_ptr<char[]> tfname(GetFileName(lp));
        if (!anyComma(lp)) {
                Error(argerr, lp, SUPPRESS); return;
        }

        aint args[] = { /*0:start*/ 0, /*1:length*/ 0, /*2:customStart*/ -1 };
        bool opt[] = { false, false, true };
        if (!getIntArguments<3>(lp, args, opt) || args[0] < 0 || args[1] < 1 || 0x10000 <= args[1] || 0x10000 < (args[0]+args[1])) {
                Error(argerr, lp, SUPPRESS); return;
        }

        SaveCDT_Code(cdtname, tfname.get(), args[0], args[1], args[2]);
}

static void dirSAVECDTHeadless(const char* cdtname) {
        constexpr const char* argerr = "[SAVECDT] Invalid args. SAVECDT HEADLESS <cdtname>,<start>,<length>[,<sync>[,<format>]]";

        if (!anyComma(lp)) {
                Error(argerr, lp, SUPPRESS); return;
        }

        aint args[] = { /*0:start*/ 0, /*1:length*/ 0, /*2:sync*/ CDTUtil::BlockTypeData, /*3:format*/ 0 };
        bool opt[] = { false, false, true, true };
        if (!getIntArguments<4>(lp, args, opt) || args[0] < 0 || args[1] < 1 || 0x10000 <= args[1] || 0x10000 < (args[0]+args[1])) {
                Error(argerr, lp, SUPPRESS); return;
        }

        ECDTHeadlessFormat format;
        switch (args[3]) {
        case 0:
                format = ECDTHeadlessFormat::AMSTRAD;
                break;
        case 1:
                format = ECDTHeadlessFormat::SPECTRUM;
                break;
        default:
                Error("[SAVECDT HEADLESS] invalid format flag. Expected 0 (AMSTRAD) or 1 (SPECTRUM).", NULL, SUPPRESS); return;
        }

        SaveCDT_Headless(cdtname, args[0], args[1], args[2], format);
}

static void cdtParseFnameAndExecuteCmd(savecdt_command_t command_fn) {
        std::unique_ptr<char[]> cdtname(GetOutputFileName(lp));
        if (cdtname[0]) command_fn(cdtname.get());
        else Error("[SAVECDT] CDT file name is empty", bp, SUPPRESS);
}

void dirSAVECDT() {
        if (pass != LASTPASS) {
                SkipToEol(lp);
                return;
        }
        if (!IsAmstradCPCDevice(DeviceID)) {
                Error("[SAVECDT] is allowed only in AMSTRADCPC464 or AMSTRADCPC6128 device mode", NULL, SUPPRESS);
                return;
        }
        SkipBlanks(lp);
        if (cmphstr(lp, "full")) cdtParseFnameAndExecuteCmd(dirSAVECDTFull);
        else if (cmphstr(lp, "empty")) cdtParseFnameAndExecuteCmd(dirSAVECDTEmpty);
        else if (cmphstr(lp, "basic")) cdtParseFnameAndExecuteCmd(dirSAVECDTBasic);
        else if (cmphstr(lp, "code")) cdtParseFnameAndExecuteCmd(dirSAVECDTCode);
        else if (cmphstr(lp, "headless")) cdtParseFnameAndExecuteCmd(dirSAVECDTHeadless);
        else Error("[SAVECDT] unknown command (commands: FULL, EMPTY, BASIC, CODE, HEADLESS)", lp, SUPPRESS);  
}

// eof io_cpc.cpp