From 21ff152c4975df78263666df968803bfa7dfc349 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Sat, 3 Jun 2023 21:03:07 +0200 Subject: [PATCH] added limited GameLine Master Module bankswitching support --- Changes.txt | 2 + docs/index.html | 1 + src/debugger/gui/Cart4KWidget.cxx | 10 -- src/debugger/gui/CartEnhancedWidget.cxx | 16 ++- src/debugger/gui/CartGLWidget.cxx | 50 +++++++ src/debugger/gui/CartGLWidget.hxx | 50 +++++++ src/debugger/gui/module.mk | 1 + src/emucore/Bankswitch.cxx | 6 +- src/emucore/Bankswitch.hxx | 4 +- src/emucore/Cart.hxx | 6 +- src/emucore/CartCreator.cxx | 3 + src/emucore/CartDetector.cxx | 17 ++- src/emucore/CartDetector.hxx | 5 + src/emucore/CartEnhanced.cxx | 16 ++- src/emucore/CartEnhanced.hxx | 13 +- src/emucore/CartGL.cxx | 133 +++++++++++++++++ src/emucore/CartGL.hxx | 134 ++++++++++++++++++ src/emucore/module.mk | 1 + src/os/windows/Stella.vcxproj | 4 + src/os/windows/Stella.vcxproj.filters | 12 ++ ...(Control Video Corporation) (fixed V2).bin | Bin 0 -> 4096 bytes test/roms/bankswitching/GL/download_rom.bin | Bin 0 -> 6144 bytes test/roms/bankswitching/GL/ramdata_rom.bin | Bin 0 -> 6144 bytes 23 files changed, 453 insertions(+), 31 deletions(-) create mode 100644 src/debugger/gui/CartGLWidget.cxx create mode 100644 src/debugger/gui/CartGLWidget.hxx create mode 100644 src/emucore/CartGL.cxx create mode 100644 src/emucore/CartGL.hxx create mode 100644 test/roms/bankswitching/GL/GameLine Master Module ROM (1983) (Control Video Corporation) (fixed V2).bin create mode 100644 test/roms/bankswitching/GL/download_rom.bin create mode 100644 test/roms/bankswitching/GL/ramdata_rom.bin diff --git a/Changes.txt b/Changes.txt index 2d44ec96e..eb605d984 100644 --- a/Changes.txt +++ b/Changes.txt @@ -34,6 +34,8 @@ * Acclerated emulation up to ~15% (ARM). + * Added limited GameLine Master Module bankswitching support. + * Added BUS bankswitching support for some older demos. * Fixed broken 7800 pause key support. diff --git a/docs/index.html b/docs/index.html index 51d20a55a..b95740a3c 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4947,6 +4947,7 @@ Ms Pac-Man (Stella extended codes): FA2 CBS RAM Plus 24/28K .FA2 FC Amiga Power Play Aracde 16/32K .FC FE 8K Activision (aka SCABS).FE + GL GameLine Master Module.GL MDM Menu Driven Megacart .MDM MVC Movie Cart.MVC SB 128-256K SUPERbanking .SB diff --git a/src/debugger/gui/Cart4KWidget.cxx b/src/debugger/gui/Cart4KWidget.cxx index 4d9c75b69..bfd9bba00 100644 --- a/src/debugger/gui/Cart4KWidget.cxx +++ b/src/debugger/gui/Cart4KWidget.cxx @@ -25,16 +25,6 @@ Cartridge4KWidget::Cartridge4KWidget( : CartridgeEnhancedWidget(boss, lfont, nfont, x, y, w, h, cart) { initialize(); - - //// Eventually, we should query this from the debugger/disassembler - //uInt16 start = (cart.myImage[0xFFD] << 8) | cart.myImage[0xFFC]; - //start -= start % 0x1000; - - //ostringstream info; - //info << "Standard 4K cartridge, non-bankswitched\n" - // << "Accessible @ $" << Common::Base::HEX4 << start << " - " - // << "$" << (start + 0xFFF); - //addBaseInformation(4096, "Atari", info.str()); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/debugger/gui/CartEnhancedWidget.cxx b/src/debugger/gui/CartEnhancedWidget.cxx index 4b017a984..3cd486d5f 100644 --- a/src/debugger/gui/CartEnhancedWidget.cxx +++ b/src/debugger/gui/CartEnhancedWidget.cxx @@ -181,11 +181,20 @@ void CartridgeEnhancedWidget::bankList(uInt16 bankCount, int seg, VariantList& i { width = 0; + const bool hasRamBanks = myCart.myRamBankCount > 0; + for(int bank = 0; bank < bankCount; ++bank) { ostringstream buf; + const bool isRamBank = (bank >= myCart.romBankCount()); + const int bankNum = (bank - (isRamBank ? myCart.romBankCount() : 0)); + + buf << std::setw(bankNum < 10 ? 2 : 1) << "#" << std::dec << bankNum; + if(isRamBank) // was RAM mapped here? + buf << " RAM"; + else if (hasRamBanks) + buf << " ROM"; - buf << std::setw(bank < 10 ? 2 : 1) << "#" << std::dec << bank; if(myCart.hotspot() != 0 && myHotspotDelta > 0) buf << " " << hotspotStr(bank, seg); VarList::push_back(items, buf.str()); @@ -196,7 +205,7 @@ void CartridgeEnhancedWidget::bankList(uInt16 bankCount, int seg, VariantList& i // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void CartridgeEnhancedWidget::bankSelect(int& ypos) { - if(myCart.romBankCount() > 1) + if(myCart.romBankCount() + myCart.ramBankCount() > 1) { constexpr int xpos = 2; @@ -208,7 +217,7 @@ void CartridgeEnhancedWidget::bankSelect(int& ypos) VariantList items; int pw = 0; - bankList(myCart.romBankCount(), seg, items, pw); + bankList(myCart.romBankCount() + myCart.ramBankCount(), seg, items, pw); // create widgets ostringstream buf; @@ -249,7 +258,6 @@ string CartridgeEnhancedWidget::bankState() const int bank = myCart.getSegmentBank(seg); const bool isRamBank = (bank >= myCart.romBankCount()); - if(seg > 0) buf << " / "; diff --git a/src/debugger/gui/CartGLWidget.cxx b/src/debugger/gui/CartGLWidget.cxx new file mode 100644 index 000000000..b4b6668ce --- /dev/null +++ b/src/debugger/gui/CartGLWidget.cxx @@ -0,0 +1,50 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2023 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#include "CartGL.hxx" +#include "CartGLWidget.hxx" + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +CartridgeGLWidget::CartridgeGLWidget( + GuiObject* boss, const GUI::Font& lfont, const GUI::Font& nfont, + int x, int y, int w, int h, CartridgeGL& cart) + : CartridgeEnhancedWidget(boss, lfont, nfont, x, y, w, h, cart) +{ + initialize(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +string CartridgeGLWidget::description() +{ + ostringstream info; + + info << "GameLine Master Module cartridge, 4K ROM, 10/12K RAM\n" + << "mapped into four 1K segments\n" + << "THIS SCHEME IS NOT FULLY IMPLEMENTED OR TESTED"; + + return info.str(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +string CartridgeGLWidget::internalRamDescription() +{ + ostringstream desc; + + desc << "Accessible 1K" << " at a time"; + + return desc.str(); +} diff --git a/src/debugger/gui/CartGLWidget.hxx b/src/debugger/gui/CartGLWidget.hxx new file mode 100644 index 000000000..70dd87fe2 --- /dev/null +++ b/src/debugger/gui/CartGLWidget.hxx @@ -0,0 +1,50 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2023 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#ifndef CARTRIDGEGL_WIDGET_HXX +#define CARTRIDGEGL_WIDGET_HXX + +class CartridgeGL; + +#include "CartEnhanced.hxx" + +class CartridgeGLWidget : public CartridgeEnhancedWidget +{ + public: + CartridgeGLWidget(GuiObject* boss, const GUI::Font& lfont, + const GUI::Font& nfont, + int x, int y, int w, int h, + CartridgeGL& cart); + ~CartridgeGLWidget() override = default; + + private: + string manufacturer() override { return "Control Video Corporation (CVC)"; } + + string description() override; + + string internalRamDescription() override; + + private: + // Following constructors and assignment operators not supported + CartridgeGLWidget() = delete; + CartridgeGLWidget(const CartridgeGLWidget&) = delete; + CartridgeGLWidget(CartridgeGLWidget&&) = delete; + CartridgeGLWidget& operator=(const CartridgeGLWidget&) = delete; + CartridgeGLWidget& operator=(CartridgeGLWidget&&) = delete; +}; + +#endif diff --git a/src/debugger/gui/module.mk b/src/debugger/gui/module.mk index f0c64410c..45ea9b5ae 100644 --- a/src/debugger/gui/module.mk +++ b/src/debugger/gui/module.mk @@ -46,6 +46,7 @@ MODULE_OBJS := \ src/debugger/gui/CartFAWidget.o \ src/debugger/gui/CartFCWidget.o \ src/debugger/gui/CartFEWidget.o \ + src/debugger/gui/CartGLWidget.o \ src/debugger/gui/CartMDMWidget.o \ src/debugger/gui/CartRamWidget.o \ src/debugger/gui/CartSBWidget.o \ diff --git a/src/emucore/Bankswitch.cxx b/src/emucore/Bankswitch.cxx index 7f9ceae81..afabf2575 100644 --- a/src/emucore/Bankswitch.cxx +++ b/src/emucore/Bankswitch.cxx @@ -119,6 +119,7 @@ Bankswitch::BSList = {{ { "FA2" , "FA2 (CBS RAM Plus 24-32K)" }, { "FC" , "FC (32K Amiga)" }, { "FE" , "FE (8K Activision)" }, + { "GL" , "GL (GameLine Master Module)" }, { "MDM" , "MDM (Menu Driven Megacart)" }, { "MVC" , "MVC (Movie Cart)" }, { "SB" , "SB (128-256K SUPERbank)" }, @@ -129,7 +130,7 @@ Bankswitch::BSList = {{ { "WDSW" , "WDSW (Pink Panther, bad)" }, { "X07" , "X07 (64K AtariAge)" }, #if defined(CUSTOM_ARM) - { "CUSTOM" , "CUSTOM (ARM)" } + { "CUSTOM" , "CUSTOM (ARM)" } #endif }}; @@ -181,6 +182,7 @@ Bankswitch::Sizes = {{ { 24_KB, 32_KB }, // _FA2 { 32_KB, 32_KB }, // _FC { 8_KB, 8_KB }, // _FE + { 4_KB, 4_KB }, // _GL { 8_KB, Bankswitch::any_KB }, // _MDM { 1024_KB, Bankswitch::any_KB }, // _MVC { 128_KB, 256_KB }, // _SB @@ -270,6 +272,7 @@ Bankswitch::ExtensionMap Bankswitch::ourExtensions = { { "FA2" , Bankswitch::Type::_FA2 }, { "FC" , Bankswitch::Type::_FC }, { "FE" , Bankswitch::Type::_FE }, + { "GL" , Bankswitch::Type::_GL }, { "MDM" , Bankswitch::Type::_MDM }, { "MVC" , Bankswitch::Type::_MVC }, { "SB" , Bankswitch::Type::_SB }, @@ -329,6 +332,7 @@ Bankswitch::NameToTypeMap Bankswitch::ourNameToTypes = { { "FA2" , Bankswitch::Type::_FA2 }, { "FC" , Bankswitch::Type::_FC }, { "FE" , Bankswitch::Type::_FE }, + { "GL" , Bankswitch::Type::_GL }, { "MDM" , Bankswitch::Type::_MDM }, { "MVC" , Bankswitch::Type::_MVC }, { "SB" , Bankswitch::Type::_SB }, diff --git a/src/emucore/Bankswitch.hxx b/src/emucore/Bankswitch.hxx index 229f30e08..d7eebd9a4 100644 --- a/src/emucore/Bankswitch.hxx +++ b/src/emucore/Bankswitch.hxx @@ -43,8 +43,8 @@ class Bankswitch _4K, _4KSC, _AR, _BF, _BFSC, _BUS, _CDF, _CM, _CTY, _CV, _DF, _DFSC, _DPC, _DPCP, _E0, _E7, _EF, _EFSC, _F0, _F4, _F4SC, _F6, _F6SC, _F8, - _F8SC, _FA, _FA2, _FC, _FE, _MDM, _MVC, _SB, - _TVBOY, _UA, _UASW, _WD, _WDSW, _X07, + _F8SC, _FA, _FA2, _FC, _FE, _GL, _MDM, _MVC, + _SB, _TVBOY, _UA, _UASW, _WD, _WDSW, _X07, #ifdef CUSTOM_ARM _CUSTOM, #endif diff --git a/src/emucore/Cart.hxx b/src/emucore/Cart.hxx index be56e4d10..b5f6555ca 100644 --- a/src/emucore/Cart.hxx +++ b/src/emucore/Cart.hxx @@ -434,6 +434,9 @@ class Cartridge : public Device // Callback to output messages messageCallback myMsgCallback{nullptr}; + // Semi-random values to use when a read from write port occurs + std::array myRWPRandomValues; + private: // The startup bank to use (where to look for the reset vector address) uInt16 myStartBank{0}; @@ -442,9 +445,6 @@ class Cartridge : public Device // by the debugger, when disassembling/dumping ROM. bool myHotspotsLocked{false}; - // Semi-random values to use when a read from write port occurs - std::array myRWPRandomValues; - // Contains various info about this cartridge // This needs to be stored separately from child classes, since // sometimes the information in both do not match diff --git a/src/emucore/CartCreator.cxx b/src/emucore/CartCreator.cxx index 04e742310..111695afe 100644 --- a/src/emucore/CartCreator.cxx +++ b/src/emucore/CartCreator.cxx @@ -54,6 +54,7 @@ #include "CartFA2.hxx" #include "CartFC.hxx" #include "CartFE.hxx" +#include "CartGL.hxx" #include "CartMDM.hxx" #include "CartMVC.hxx" #include "CartSB.hxx" @@ -305,6 +306,8 @@ CartCreator::createFromImage(const ByteBuffer& image, size_t size, return make_unique(image, size, md5, settings); case Bankswitch::Type::_FE: return make_unique(image, size, md5, settings); + case Bankswitch::Type::_GL: + return make_unique(image, size, md5, settings); case Bankswitch::Type::_MDM: return make_unique(image, size, md5, settings); case Bankswitch::Type::_UA: diff --git a/src/emucore/CartDetector.cxx b/src/emucore/CartDetector.cxx index ced6c349d..2d611290e 100644 --- a/src/emucore/CartDetector.cxx +++ b/src/emucore/CartDetector.cxx @@ -27,9 +27,12 @@ Bankswitch::Type CartDetector::autodetectType(const ByteBuffer& image, size_t si // Guess type based on size Bankswitch::Type type = Bankswitch::Type::_AUTO; - if((size % 8448) == 0 || size == 6144) + if((size % 8448) == 0 || size == 6_KB) { - type = Bankswitch::Type::_AR; + if(size == 6_KB && isProbablyGL(image, size)) + type = Bankswitch::Type::_GL; + else + type = Bankswitch::Type::_AR; } else if(size < 2_KB) // Sub2K images { @@ -48,6 +51,8 @@ Bankswitch::Type CartDetector::autodetectType(const ByteBuffer& image, size_t si type = Bankswitch::Type::_4KSC; else if (isProbablyFC(image, size)) type = Bankswitch::Type::_FC; + else if (isProbablyGL(image, size)) + type = Bankswitch::Type::_GL; else type = Bankswitch::Type::_4K; } @@ -706,6 +711,14 @@ bool CartDetector::isProbablyFE(const ByteBuffer& image, size_t size) return false; } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool CartDetector::isProbablyGL(const ByteBuffer& image, size_t size) +{ + static constexpr uInt8 signature[] = { 0xad, 0xb8, 0x0c }; // LDA $0CB8 + + return searchForBytes(image, size, signature, 3); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool CartDetector::isProbablyMDM(const ByteBuffer& image, size_t size) { diff --git a/src/emucore/CartDetector.hxx b/src/emucore/CartDetector.hxx index 9e5716888..e2d346f0a 100644 --- a/src/emucore/CartDetector.hxx +++ b/src/emucore/CartDetector.hxx @@ -201,6 +201,11 @@ class CartDetector */ static bool isProbablyFE(const ByteBuffer& image, size_t size); + /** + Returns true if the image is probably a GameLine cartridge + */ + static bool isProbablyGL(const ByteBuffer& image, size_t size); + /** Returns true if the image is probably a MDM bankswitching cartridge */ diff --git a/src/emucore/CartEnhanced.cxx b/src/emucore/CartEnhanced.cxx index c15727f6c..fa983f0fb 100644 --- a/src/emucore/CartEnhanced.cxx +++ b/src/emucore/CartEnhanced.cxx @@ -65,8 +65,11 @@ CartridgeEnhanced::CartridgeEnhanced(const ByteBuffer& image, size_t size, // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void CartridgeEnhanced::install(System& system) { + if(!myRamBankShift) + myRamBankShift = myBankShift - 1; + // limit banked RAM size to the size of one RAM bank - const uInt16 ramSize = myRamBankCount > 0 ? 1 << (myBankShift - 1) : + const uInt16 ramSize = myRamBankCount > 0 ? 1 << myRamBankShift : static_cast(myRamSize); // calculate bank switching and RAM sizes and masks @@ -200,7 +203,8 @@ bool CartridgeEnhanced::poke(uInt16 address, uInt8 value) if(isRamBank(address)) { - if(static_cast(address & (myBankSize >> 1)) == myRamWpHigh) + if(static_cast(address & (myBankSize >> 1)) == myRamWpHigh + || myBankShift == myRamBankShift) { address &= myRamMask; // The RAM banks follow the ROM banks and are half the size of a ROM bank @@ -271,7 +275,7 @@ bool CartridgeEnhanced::bank(uInt16 bank, uInt16 segment) const uInt16 ramBank = (bank - romBankCount()) % myRamBankCount; // The RAM banks follow the ROM banks and are half the size of a ROM bank const uInt32 bankOffset = static_cast(mySize) + - (ramBank << (myBankShift - 1)); + (ramBank << myRamBankShift); // Remember what bank is in this segment myCurrentSegOffset[segment] = static_cast(mySize) + @@ -280,7 +284,8 @@ bool CartridgeEnhanced::bank(uInt16 bank, uInt16 segment) // Set the page accessing method for the RAM writing pages // Note: Writes are mapped to poke() (NOT using directPokeBase) to check for read from write port (RWP) uInt16 fromAddr = (ROM_OFFSET + segmentOffset + myWriteOffset) & ~System::PAGE_MASK; - uInt16 toAddr = (ROM_OFFSET + segmentOffset + myWriteOffset + (myBankSize >> 1)) & ~System::PAGE_MASK; + uInt16 toAddr = (ROM_OFFSET + segmentOffset + myWriteOffset + + (myBankSize >> (myBankShift - myRamBankShift))) & ~System::PAGE_MASK; System::PageAccess access(this, System::PageAccessType::WRITE); for(uInt16 addr = fromAddr; addr < toAddr; addr += System::PAGE_SIZE) @@ -295,7 +300,8 @@ bool CartridgeEnhanced::bank(uInt16 bank, uInt16 segment) // Set the page accessing method for the RAM reading pages fromAddr = (ROM_OFFSET + segmentOffset + myReadOffset) & ~System::PAGE_MASK; - toAddr = (ROM_OFFSET + segmentOffset + myReadOffset + (myBankSize >> 1)) & ~System::PAGE_MASK; + toAddr = (ROM_OFFSET + segmentOffset + myReadOffset + + (myBankSize >> (myBankShift - myRamBankShift))) & ~System::PAGE_MASK; access.type = System::PageAccessType::READ; for(uInt16 addr = fromAddr; addr < toAddr; addr += System::PAGE_SIZE) diff --git a/src/emucore/CartEnhanced.hxx b/src/emucore/CartEnhanced.hxx index 4d47411f2..d45c0c758 100644 --- a/src/emucore/CartEnhanced.hxx +++ b/src/emucore/CartEnhanced.hxx @@ -214,6 +214,9 @@ class CartridgeEnhanced : public Cartridge // The mask for a bank segment uInt16 myBankMask{ROM_MASK}; + // Usually myBankShift - 1 + uInt16 myRamBankShift{0}; + protected: // The extra RAM size size_t myRamSize{RAM_SIZE}; // default 0 @@ -318,7 +321,7 @@ class CartridgeEnhanced : public Cartridge virtual uInt16 getStartBank() const { return 0; } /** - Get the ROM offset of the segment of the given address + Get the ROM offset of the segment of the given address. @param address The address to get the offset for @return The calculated offset @@ -328,14 +331,16 @@ class CartridgeEnhanced : public Cartridge } /** - Get the RAM offset of the segment of the given address + Get the RAM offset of the segment of the given address. + The RAM banks are half the size of a ROM bank. @param address The address to get the offset for @return The calculated offset */ uInt16 ramAddressSegmentOffset(uInt16 address) const { - return static_cast((myCurrentSegOffset[ - ((address & ROM_MASK) >> myBankShift) % myBankSegs] - mySize) >> 1); + return static_cast( + (myCurrentSegOffset[((address & ROM_MASK) >> myBankShift) % myBankSegs] - mySize) + >> (myBankShift - myRamBankShift)); } private: diff --git a/src/emucore/CartGL.cxx b/src/emucore/CartGL.cxx new file mode 100644 index 000000000..bf9d9c496 --- /dev/null +++ b/src/emucore/CartGL.cxx @@ -0,0 +1,133 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2023 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#include "System.hxx" +#include "CartGL.hxx" + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +CartridgeGL::CartridgeGL(const ByteBuffer& image, size_t size, + string_view md5, const Settings& settings, + size_t bsSize) + : CartridgeEnhanced(image, size, md5, settings, bsSize) +{ + myBankShift = myRamBankShift = BANK_SHIFT; + myRamSize = RAM_SIZE; + myRamBankCount = RAM_BANKS; + + if(size == 4_KB + 2_KB) // ROM containing RAM data? + { + myInitialRAM = make_unique(2_KB); + // Copy the RAM image into a buffer for use in reset() + std::copy_n(image.get() + 4_KB, 2_KB, myInitialRAM.get()); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void CartridgeGL::reset() +{ + CartridgeEnhanced::reset(); + + // Initially bank 0 is mapped into all four segments + bank(0, 0); + bank(0, 1); + bank(0, 2); + bank(0, 3); + myBankChanged = true; + + myOrgAccess = mySystem->getPageAccess(0x1fc0); + + initializeRAM(myRAM.get(), myRamSize); + if(myInitialRAM != nullptr) + { + // Copy the RAM image into my RAM buffer + std::copy_n(myInitialRAM.get(), 2_KB, myRAM.get()); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void CartridgeGL::install(System& system) +{ + CartridgeEnhanced::install(system); + + System::PageAccess access(this, System::PageAccessType::READ); + + mySystem->setPageAccess(0x480, access); + mySystem->setPageAccess(0x580, access); + mySystem->setPageAccess(0x680, access); + mySystem->setPageAccess(0x880, access); + mySystem->setPageAccess(0x980, access); + mySystem->setPageAccess(0xc80, access); + mySystem->setPageAccess(0xd80, access); + + myReadOffset = myWriteOffset = 0; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool CartridgeGL::checkSwitchBank(uInt16 address, uInt8) +{ + int slice = -1; + bool control = false; + + // Switch banks if necessary + switch(address & 0x1f80) + { + case 0x480: + slice = 0; + break; + case 0x580: + slice = 1; + break; + case 0x880: + slice = 2; + break; + case 0x980: + slice = 3; + break; + case 0xc80: + control = true; + break; + } + if(slice >= 0) + { + //const bool isWrite = address & 0x20; // could be checked, but not necessary for known GL ROMs + bank(address & 0xf, slice); + return true; + } + if(control) + { + myEnablePROM = (address & 0x30) == 0x30; + if(myEnablePROM) + mySystem->setPageAccess(0x1fc0, System::PageAccess(this, System::PageAccessType::READ)); + else + mySystem->setPageAccess(0x1fc0, myOrgAccess); + return true; + } + return false; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt8 CartridgeGL::peek(uInt16 address) +{ + if(myEnablePROM && ((address & ADDR_MASK) >= 0x1fc0) && ((address & ADDR_MASK) <= 0x1fdf)) + { + return 0; // sufficient for PROM check + } + + checkSwitchBank(address, 0); + + return myRWPRandomValues[address & 0xFF]; +} diff --git a/src/emucore/CartGL.hxx b/src/emucore/CartGL.hxx new file mode 100644 index 000000000..77f34d1b7 --- /dev/null +++ b/src/emucore/CartGL.hxx @@ -0,0 +1,134 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2023 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#ifndef CARTRIDGEGL_HXX +#define CARTRIDGEGL_HXX + +#include "CartEnhanced.hxx" +#ifdef DEBUGGER_SUPPORT + #include "CartGLWidget.hxx" +#endif +#include "System.hxx" + +/** + Cartridge class used for the GameLine Master module. In this bankswitching + scheme the 2600's 4K cartridge address space is broken into four 1K segments. + The desired 1K bank of the ROM or RAM is selected as follows: + - $0480 + x: 1st 1K segment + - $0580 + x: 2nd 1K segment + - $0880 + x: 3rd 1K segment + - $0980 + x: 4th 1K segment + Where x is defined as follows: + - bits 0..3: mapped 1K bank (0..3 = ROM bank, 4..f = RAM bank) + - bit 5: 0 = read, 1 = write (RAM only) + Initially bank 0 is mapped to all four segments. + The scheme supports 4K ROM and 2K RAM. + + $0c80.. and $0d80.. control the modem (not implemented, except for PROM access). + + @author Thomas Jentzsch +*/ +class CartridgeGL : public CartridgeEnhanced +{ + friend class CartridgeGLWidget; + + public: + /** + Create a new cartridge using the specified image + + @param image Pointer to the ROM image + @param size The size of the ROM image + @param md5 The md5sum of the ROM image + @param settings A reference to the various settings (read-only) + @param bsSize The size specified by the bankswitching scheme + */ + CartridgeGL(const ByteBuffer& image, size_t size, string_view md5, + const Settings& settings, size_t bsSize = 4_KB); + ~CartridgeGL() override = default; + + public: + /** + Reset device to its power-on state + */ + void reset() override; + + /** + Install cartridge in the specified system. Invoked by the system + when the cartridge is attached to it. + + @param system The system the device should install itself in + */ + void install(System& system) override; + + /** + Get a descriptor for the device name (used in error checking). + + @return The name of the object + */ + string name() const override { return "CartridgeGL"; } + + #ifdef DEBUGGER_SUPPORT + /** + Get debugger widget responsible for accessing the inner workings + of the cart. + */ + CartDebugWidget* debugWidget(GuiObject* boss, const GUI::Font& lfont, + const GUI::Font& nfont, int x, int y, int w, int h) override + { + return new CartridgeGLWidget(boss, lfont, nfont, x, y, w, h, *this); + } + #endif + + public: + /** + Get the byte at the specified address. + + @return The byte at the specified address + */ + uInt8 peek(uInt16 address) override; + + private: + bool checkSwitchBank(uInt16 address, uInt8) override; + + protected: + // log(ROM bank segment size) / log(2) + static constexpr uInt16 BANK_SHIFT = 10; // = 1K = 0x0400 + + // The number of RAM banks + static constexpr uInt16 RAM_BANKS = 12; + + // RAM size + static constexpr size_t RAM_SIZE = RAM_BANKS << BANK_SHIFT; // = 12K; + + private: + // Initial RAM data from the cart (doesn't always exist) + ByteBuffer myInitialRAM{nullptr}; + + bool myEnablePROM{false}; + + System::PageAccess myOrgAccess; + + private: + // Following constructors and assignment operators not supported + CartridgeGL() = delete; + CartridgeGL(const CartridgeGL&) = delete; + CartridgeGL(CartridgeGL&&) = delete; + CartridgeGL& operator=(const CartridgeGL&) = delete; + CartridgeGL& operator=(CartridgeGL&&) = delete; +}; + +#endif diff --git a/src/emucore/module.mk b/src/emucore/module.mk index c85875e8b..be23f42ee 100644 --- a/src/emucore/module.mk +++ b/src/emucore/module.mk @@ -46,6 +46,7 @@ MODULE_OBJS := \ src/emucore/CartFA2.o \ src/emucore/CartFC.o \ src/emucore/CartFE.o \ + src/emucore/CartGL.o \ src/emucore/CartMDM.o \ src/emucore/CartMVC.o \ src/emucore/CartSB.o \ diff --git a/src/os/windows/Stella.vcxproj b/src/os/windows/Stella.vcxproj index 1b88ac41f..05ab4683f 100755 --- a/src/os/windows/Stella.vcxproj +++ b/src/os/windows/Stella.vcxproj @@ -911,6 +911,7 @@ true + true @@ -996,6 +997,7 @@ + @@ -2223,6 +2225,7 @@ true + true @@ -2319,6 +2322,7 @@ + diff --git a/src/os/windows/Stella.vcxproj.filters b/src/os/windows/Stella.vcxproj.filters index 071b641e8..7d3849df5 100644 --- a/src/os/windows/Stella.vcxproj.filters +++ b/src/os/windows/Stella.vcxproj.filters @@ -1197,6 +1197,12 @@ Source Files\debugger + + Source Files\emucore + + + Source Files\debugger\gui + @@ -2441,6 +2447,12 @@ Header Files\debugger + + Header Files\emucore + + + Header Files\debugger\gui + diff --git a/test/roms/bankswitching/GL/GameLine Master Module ROM (1983) (Control Video Corporation) (fixed V2).bin b/test/roms/bankswitching/GL/GameLine Master Module ROM (1983) (Control Video Corporation) (fixed V2).bin new file mode 100644 index 0000000000000000000000000000000000000000..d9ad832be8bd47b30d10e0c3b2e3f96599e26aa9 GIT binary patch literal 4096 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 literal 0 HcmV?d00001 diff --git a/test/roms/bankswitching/GL/download_rom.bin b/test/roms/bankswitching/GL/download_rom.bin new file mode 100644 index 0000000000000000000000000000000000000000..e5b1c3c7dd959b3e40d8f387978d836a522da5e7 GIT binary patch literal 6144 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 literal 0 HcmV?d00001 diff --git a/test/roms/bankswitching/GL/ramdata_rom.bin b/test/roms/bankswitching/GL/ramdata_rom.bin new file mode 100644 index 0000000000000000000000000000000000000000..73b1e62621c391924b3017a4fc8dedf55c6ace0d GIT binary patch literal 6144 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

GV`?CLNueozBf1Pt4EG&O}Y~%Iw_2e3@>VUznXd zfvbiEFr^cTw`WgA$;|9rLYYdw4PBzf{~bfCOr?|a>BMAmYARY8iys;piAT+orzYdC z#70J<+T=_saXbeGeR4iZE@ab*8JImWn~72|=v>s4NuHXW%Ppj%Iy__oW}Zt&qftI} zoFAR$`PaZlGNXJc8RRE<{x-|x?99U4>{PVsR4$dCjgphIb0BweAqOhx(ql*AH}Trh zv1n={s(C%JFbi_cOs1!znxhywKAT8p)5%m+lg*8<%!sv|OC%Ce&3t+aRtN@~z|-3C PF8^cs^7lX0t9t(j{}*MD literal 0 HcmV?d00001