diff --git a/Changes.txt b/Changes.txt index 697fe0511..5f9863c3a 100644 --- a/Changes.txt +++ b/Changes.txt @@ -48,6 +48,10 @@ * Added automatic controller detection. (TODO: Stella.pro cleanup) + * Added 'HiDPI' mode, which scales the UI by 2x when enabled. This is + meant for 4k and above monitors, but can actually be used at any + lower resolution that is large enough to display the scaled UI. + * Removed 'tia.fsfill' option, replacing it with 'tia.fs_stretch'. This new option allows to preserve TIA image aspect ratio in fullscreen mode, or stretch to fill the entire screen. diff --git a/src/common/repository/sqlite/KeyValueRepositorySqlite.cxx b/src/common/repository/sqlite/KeyValueRepositorySqlite.cxx index c57293ff8..c6155e258 100644 --- a/src/common/repository/sqlite/KeyValueRepositorySqlite.cxx +++ b/src/common/repository/sqlite/KeyValueRepositorySqlite.cxx @@ -18,6 +18,7 @@ #include "KeyValueRepositorySqlite.hxx" #include "Logger.hxx" #include "SqliteError.hxx" +#include "SqliteTransaction.hxx" // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - KeyValueRepositorySqlite::KeyValueRepositorySqlite( @@ -51,9 +52,9 @@ std::map KeyValueRepositorySqlite::load() void KeyValueRepositorySqlite::save(const std::map& values) { try { - myStmtInsert->reset(); + SqliteTransaction tx(myDb); - myDb.exec("BEGIN TRANSACTION"); + myStmtInsert->reset(); for (const auto& pair: values) { (*myStmtInsert) @@ -64,7 +65,7 @@ void KeyValueRepositorySqlite::save(const std::map& values) myStmtInsert->reset(); } - myDb.exec("COMMIT"); + tx.commit(); } catch (SqliteError err) { Logger::log(err.message, 1); diff --git a/src/common/repository/sqlite/SqliteTransaction.cxx b/src/common/repository/sqlite/SqliteTransaction.cxx new file mode 100644 index 000000000..34882e9dd --- /dev/null +++ b/src/common/repository/sqlite/SqliteTransaction.cxx @@ -0,0 +1,51 @@ +//============================================================================ +// +// 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-2019 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 "SqliteTransaction.hxx" +#include "SqliteDatabase.hxx" + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +SqliteTransaction::SqliteTransaction(SqliteDatabase& db) + : myDb(db), + myTransactionClosed(false) +{ + myDb.exec("BEGIN TRANSACTION"); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +SqliteTransaction::~SqliteTransaction() +{ + if (!myTransactionClosed) rollback(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void SqliteTransaction::commit() +{ + if (myTransactionClosed) return; + + myTransactionClosed = true; + myDb.exec("COMMIT TRANSACTION"); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void SqliteTransaction::rollback() +{ + if (myTransactionClosed) return; + + myTransactionClosed = true; + myDb.exec("ROLLBACK TRANSACTION"); +} diff --git a/src/common/repository/sqlite/SqliteTransaction.hxx b/src/common/repository/sqlite/SqliteTransaction.hxx new file mode 100644 index 000000000..f4e0df342 --- /dev/null +++ b/src/common/repository/sqlite/SqliteTransaction.hxx @@ -0,0 +1,48 @@ +//============================================================================ +// +// 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-2019 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 SQLITE_TRANSACTION_HXX +#define SQLITE_TRANSACTION_HXX + +class SqliteDatabase; + +class SqliteTransaction { + public: + + SqliteTransaction(SqliteDatabase& db); + + ~SqliteTransaction(); + + void commit(); + + void rollback(); + + private: + + SqliteDatabase& myDb; + + bool myTransactionClosed; + + private: + + SqliteTransaction(const SqliteTransaction&) = delete; + SqliteTransaction(SqliteTransaction&&) = delete; + SqliteTransaction& operator=(const SqliteTransaction&) = delete; + SqliteTransaction& operator=(SqliteTransaction&&) = delete; +}; + +#endif // SQLITE_TRANSACTION_HXX diff --git a/src/common/repository/sqlite/module.mk b/src/common/repository/sqlite/module.mk index ef6f5b207..0c2192960 100644 --- a/src/common/repository/sqlite/module.mk +++ b/src/common/repository/sqlite/module.mk @@ -4,7 +4,8 @@ MODULE_OBJS := \ src/common/repository/sqlite/KeyValueRepositorySqlite.o \ src/common/repository/sqlite/SettingsDb.o \ src/common/repository/sqlite/SqliteDatabase.o \ - src/common/repository/sqlite/SqliteStatement.o + src/common/repository/sqlite/SqliteStatement.o \ + src/common/repository/sqlite/SqliteTransaction.o MODULE_DIRS += \ src/common/repository/sqlite diff --git a/src/debugger/gui/RamWidget.cxx b/src/debugger/gui/RamWidget.cxx index 6bbfcbced..9d0647fc9 100644 --- a/src/debugger/gui/RamWidget.cxx +++ b/src/debugger/gui/RamWidget.cxx @@ -23,6 +23,7 @@ #include "Debugger.hxx" #include "CartDebug.hxx" #include "Font.hxx" +#include "FBSurface.hxx" #include "Widget.hxx" #include "RamWidget.hxx" @@ -330,7 +331,7 @@ void RamWidget::showInputBox(int cmd) uInt32 x = getAbsX() + ((getWidth() - myInputBox->getWidth()) >> 1); uInt32 y = getAbsY() + ((getHeight() - myInputBox->getHeight()) >> 1); - myInputBox->show(x, y); + myInputBox->show(x, y, dialog().surface().dstRect()); myInputBox->setText(""); myInputBox->setMessage(""); myInputBox->setFocus(0); diff --git a/src/debugger/gui/TiaOutputWidget.cxx b/src/debugger/gui/TiaOutputWidget.cxx index b70295770..e05247819 100644 --- a/src/debugger/gui/TiaOutputWidget.cxx +++ b/src/debugger/gui/TiaOutputWidget.cxx @@ -48,7 +48,9 @@ TiaOutputWidget::TiaOutputWidget(GuiObject* boss, const GUI::Font& font, VarList::push_back(l, "Fill to scanline", "scanline"); VarList::push_back(l, "Toggle breakpoint", "bp"); VarList::push_back(l, "Set zoom position", "zoom"); +#ifdef PNG_SUPPORT VarList::push_back(l, "Save snapshot", "snap"); +#endif myMenu = make_unique(this, font, l); } diff --git a/src/emucore/OSystem.cxx b/src/emucore/OSystem.cxx index 3f94bc1d9..879299ab1 100644 --- a/src/emucore/OSystem.cxx +++ b/src/emucore/OSystem.cxx @@ -262,8 +262,11 @@ void OSystem::setConfigPaths() buildDirIfRequired(myStateDir, myBaseDir + "state"); buildDirIfRequired(myNVRamDir, myBaseDir + "nvram"); +#ifdef DEBUGGER_SUPPORT buildDirIfRequired(myCfgDir, myBaseDir + "cfg"); +#endif +#ifdef PNG_SUPPORT mySnapshotSaveDir = mySettings->getString("snapsavedir"); if(mySnapshotSaveDir == "") mySnapshotSaveDir = defaultSaveDir(); buildDirIfRequired(mySnapshotSaveDir, mySnapshotSaveDir); @@ -271,6 +274,7 @@ void OSystem::setConfigPaths() mySnapshotLoadDir = mySettings->getString("snaploaddir"); if(mySnapshotLoadDir == "") mySnapshotLoadDir = defaultLoadDir(); buildDirIfRequired(mySnapshotLoadDir, mySnapshotLoadDir); +#endif myCheatFile = FilesystemNode(myBaseDir + "stella.cht").getPath(); myPaletteFile = FilesystemNode(myBaseDir + "stella.pal").getPath(); diff --git a/src/emucore/OSystem.hxx b/src/emucore/OSystem.hxx index 46a750ec0..302deebb9 100644 --- a/src/emucore/OSystem.hxx +++ b/src/emucore/OSystem.hxx @@ -174,15 +174,6 @@ class OSystem */ void saveConfig(); - #ifdef DEBUGGER_SUPPORT - /** - Get the ROM debugger of the system. - - @return The debugger object - */ - Debugger& debugger() const { return *myDebugger; } - #endif - #ifdef CHEATCODE_SUPPORT /** Get the cheat manager of the system. @@ -192,13 +183,13 @@ class OSystem CheatManager& cheat() const { return *myCheatManager; } #endif - #ifdef PNG_SUPPORT + #ifdef DEBUGGER_SUPPORT /** - Get the PNG handler of the system. + Get the ROM debugger of the system. - @return The PNGlib object + @return The debugger object */ - PNGLibrary& png() const { return *myPNGLib; } + Debugger& debugger() const { return *myDebugger; } #endif #ifdef GUI_SUPPORT @@ -231,6 +222,15 @@ class OSystem TimeMachine& timeMachine() const { return *myTimeMachine; } #endif + #ifdef PNG_SUPPORT + /** + Get the PNG handler of the system. + + @return The PNGlib object + */ + PNGLibrary& png() const { return *myPNGLib; } + #endif + /** Set all config file paths for the OSystem. */ @@ -246,30 +246,36 @@ class OSystem */ const string& stateDir() const { return myStateDir; } - /** - Return the full/complete directory name for saving and loading - PNG snapshots. - */ - const string& snapshotSaveDir() const { return mySnapshotSaveDir; } - const string& snapshotLoadDir() const { return mySnapshotLoadDir; } - /** Return the full/complete directory name for storing nvram (flash/EEPROM) files. */ const string& nvramDir() const { return myNVRamDir; } - /** - Return the full/complete directory name for storing Distella cfg files. - */ - const string& cfgDir() const { return myCfgDir; } - + #ifdef CHEATCODE_SUPPORT /** This method should be called to get the full path of the cheat file. @return String representing the full path of the cheat filename. */ const string& cheatFile() const { return myCheatFile; } + #endif + + #ifdef DEBUGGER_SUPPORT + /** + Return the full/complete directory name for storing Distella cfg files. + */ + const string& cfgDir() const { return myCfgDir; } + #endif + + #ifdef PNG_SUPPORT + /** + Return the full/complete directory name for saving and loading + PNG snapshots. + */ + const string& snapshotSaveDir() const { return mySnapshotSaveDir; } + const string& snapshotLoadDir() const { return mySnapshotLoadDir; } + #endif /** This method should be called to get the full path of the @@ -483,6 +489,16 @@ class OSystem // Pointer to audio settings object unique_ptr myAudioSettings; + #ifdef CHEATCODE_SUPPORT + // Pointer to the CheatManager object + unique_ptr myCheatManager; + #endif + + #ifdef DEBUGGER_SUPPORT + // Pointer to the Debugger object + unique_ptr myDebugger; + #endif + #ifdef GUI_SUPPORT // Pointer to the Menu object unique_ptr myMenu; @@ -497,16 +513,6 @@ class OSystem unique_ptr myTimeMachine; #endif - #ifdef DEBUGGER_SUPPORT - // Pointer to the Debugger object - unique_ptr myDebugger; - #endif - - #ifdef CHEATCODE_SUPPORT - // Pointer to the CheatManager object - unique_ptr myCheatManager; - #endif - #ifdef PNG_SUPPORT // PNG object responsible for loading/saving PNG images unique_ptr myPNGLib; diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index 256d432cc..e14566475 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -885,17 +885,18 @@ Widget* Dialog::TabFocus::getNewFocus() bool Dialog::getDynamicBounds(uInt32& w, uInt32& h) const { const Common::Rect& r = instance().frameBuffer().imageRect(); + const uInt32 scale = instance().frameBuffer().hidpiScaleFactor(); if(r.width() <= FBMinimum::Width || r.height() <= FBMinimum::Height) { - w = r.width(); - h = r.height(); + w = r.width() / scale; + h = r.height() / scale; return false; } else { - w = uInt32(0.95 * r.width()); - h = uInt32(0.95 * r.height()); + w = uInt32(0.95 * r.width() / scale); + h = uInt32(0.95 * r.height() / scale); return true; } } diff --git a/src/gui/GlobalPropsDialog.cxx b/src/gui/GlobalPropsDialog.cxx index c12b8c0a5..d8911feda 100644 --- a/src/gui/GlobalPropsDialog.cxx +++ b/src/gui/GlobalPropsDialog.cxx @@ -96,7 +96,9 @@ GlobalPropsDialog::GlobalPropsDialog(GuiObject* boss, const GUI::Font& font) new StaticTextWidget(this, font, xpos, ypos+1, "Startup mode"); items.clear(); VarList::push_back(items, "Console", "false"); +#ifdef DEBUGGER_SUPPORT VarList::push_back(items, "Debugger", "true"); +#endif myDebug = new PopUpWidget(this, font, xpos+lwidth, ypos, pwidth, lineHeight, items, ""); wid.push_back(myDebug); diff --git a/src/gui/InputTextDialog.cxx b/src/gui/InputTextDialog.cxx index 983f410e8..930dc2f0a 100644 --- a/src/gui/InputTextDialog.cxx +++ b/src/gui/InputTextDialog.cxx @@ -118,10 +118,16 @@ void InputTextDialog::show() } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void InputTextDialog::show(uInt32 x, uInt32 y) +void InputTextDialog::show(uInt32 x, uInt32 y, const Common::Rect& bossRect) { - myXOrig = x; - myYOrig = y; + uInt32 scale = instance().frameBuffer().hidpiScaleFactor(); + myXOrig = bossRect.x() + x * scale; + myYOrig = bossRect.y() + y * scale; + + // Only show dialog if we're inside the visible area + if(!bossRect.contains(myXOrig, myYOrig)) + return; + myEnableCenter = false; open(); } @@ -131,17 +137,14 @@ void InputTextDialog::center() { if(!myEnableCenter) { - // Make sure the menu is exactly where it should be, in case the image - // offset has changed - const Common::Rect& image = instance().frameBuffer().imageRect(); - uInt32 x = image.x() + myXOrig; - uInt32 y = image.y() + myYOrig; - uInt32 tx = image.x() + image.width(); - uInt32 ty = image.y() + image.height(); - if(x + _w > tx) x -= (x + _w - tx); - if(y + _h > ty) y -= (y + _h - ty); + // First set position according to original coordinates + surface().setDstPos(myXOrig, myYOrig); - surface().setDstPos(x, y); + // Now make sure that the entire menu can fit inside the image bounds + // If not, we reset its position + if(!instance().frameBuffer().imageRect().contains( + myXOrig, myXOrig, surface().dstRect())) + surface().setDstPos(myXOrig, myYOrig); } else Dialog::center(); diff --git a/src/gui/InputTextDialog.hxx b/src/gui/InputTextDialog.hxx index 9c798bc14..6b8ef2eac 100644 --- a/src/gui/InputTextDialog.hxx +++ b/src/gui/InputTextDialog.hxx @@ -39,7 +39,7 @@ class InputTextDialog : public Dialog, public CommandSender void show(); /** Show input dialog onscreen at the specified coordinates */ - void show(uInt32 x, uInt32 y); + void show(uInt32 x, uInt32 y, const Common::Rect& bossRect); const string& getResult(int idx = 0); diff --git a/src/gui/RomAuditDialog.cxx b/src/gui/RomAuditDialog.cxx index 5bd2a98c9..6155ce61e 100644 --- a/src/gui/RomAuditDialog.cxx +++ b/src/gui/RomAuditDialog.cxx @@ -36,6 +36,7 @@ RomAuditDialog::RomAuditDialog(OSystem& osystem, DialogContainer& parent, const GUI::Font& font, int max_w, int max_h) : Dialog(osystem, parent, font, "Audit ROMs"), + myFont(font), myMaxWidth(max_w), myMaxHeight(max_h) { @@ -87,9 +88,6 @@ RomAuditDialog::RomAuditDialog(OSystem& osystem, DialogContainer& parent, // Add OK and Cancel buttons addOKCancelBGroup(wid, font, "Audit", "Close"); addBGroupToFocusList(wid); - - // Create file browser dialog - myBrowser = make_unique(this, font, myMaxWidth, myMaxHeight, "Select ROM directory to audit"); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -187,9 +185,8 @@ void RomAuditDialog::handleCommand(CommandSender* sender, int cmd, msg.push_back("If you're sure you want to proceed with the"); msg.push_back("audit, click 'OK', otherwise click 'Cancel'."); myConfirmMsg = make_unique - (this, instance().frameBuffer().font(), msg, - myMaxWidth, myMaxHeight, kConfirmAuditCmd, - "OK", "Cancel", "ROM Audit", false); + (this, myFont, msg, myMaxWidth, myMaxHeight, kConfirmAuditCmd, + "OK", "Cancel", "ROM Audit", false); } myConfirmMsg->show(); break; @@ -200,6 +197,7 @@ void RomAuditDialog::handleCommand(CommandSender* sender, int cmd, break; case kChooseAuditDirCmd: + createBrowser("Select ROM directory to audit"); myBrowser->show(myRomPath->getText(), BrowserDialog::Directories, kAuditDirChosenCmd); break; @@ -218,3 +216,17 @@ void RomAuditDialog::handleCommand(CommandSender* sender, int cmd, break; } } + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void RomAuditDialog::createBrowser(const string& title) +{ + uInt32 w = 0, h = 0; + getDynamicBounds(w, h); + + // Create file browser dialog + if(!myBrowser || uInt32(myBrowser->getWidth()) != w || + uInt32(myBrowser->getHeight()) != h) + myBrowser = make_unique(this, myFont, w, h, title); + else + myBrowser->setTitle(title); +} diff --git a/src/gui/RomAuditDialog.hxx b/src/gui/RomAuditDialog.hxx index a2ebba3b5..fbd82e693 100644 --- a/src/gui/RomAuditDialog.hxx +++ b/src/gui/RomAuditDialog.hxx @@ -42,8 +42,7 @@ class RomAuditDialog : public Dialog private: void loadConfig() override; void auditRoms(); - void openBrowser(const string& title, const string& startpath, - FilesystemNode::ListMode mode, int cmd); + void createBrowser(const string& title); void handleCommand(CommandSender* sender, int cmd, int data, int id) override; private: @@ -55,6 +54,7 @@ class RomAuditDialog : public Dialog // Select a new ROM audit path unique_ptr myBrowser; + const GUI::Font& myFont; // ROM audit path EditTextWidget* myRomPath; diff --git a/src/libretro/Makefile b/src/libretro/Makefile index c29106076..5f6a0348f 100644 --- a/src/libretro/Makefile +++ b/src/libretro/Makefile @@ -402,7 +402,7 @@ else ifneq (,$(findstring windows_msvc2017,$(platform))) PATH := $(PATH):$(shell IFS=$$'\n'; cygpath "$(VsInstallRoot)/Common7/IDE") INCLUDE := $(shell IFS=$$'\n'; cygpath -w "$(VcCompilerToolsDir)/include") ifneq (,$(findstring uwp,$(PlatformSuffix))) - LIB := $(shell IFS=$$'\n'; cygpath -w "$(LIB)/store") + LIB := $(shell IFS=$$'\n'; cygpath -w "$(VcCompilerToolsDir)/lib/$(TargetArchMoniker/store)") else LIB := $(shell IFS=$$'\n'; cygpath -w "$(VcCompilerToolsDir)/lib/$(TargetArchMoniker)") endif diff --git a/src/libretro/StellaLIBRETRO.cxx b/src/libretro/StellaLIBRETRO.cxx index 6e4f1c839..ffe5dfa09 100644 --- a/src/libretro/StellaLIBRETRO.cxx +++ b/src/libretro/StellaLIBRETRO.cxx @@ -62,30 +62,14 @@ bool StellaLIBRETRO::create(bool logging) FilesystemNode rom("rom"); - // auto-detect properties - destroy(); - - myOSystem = make_unique(); - myOSystem->create(); - - myOSystem->settings().setValue("format", console_format); - - if(myOSystem->createConsole(rom) != EmptyString) - return false; - - - // auto-detect settings - console_timing = myOSystem->console().timing(); - phosphor_default = myOSystem->frameBuffer().tiaSurface().phosphorEnabled(); - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // build play system destroy(); myOSystem = make_unique(); myOSystem->create(); + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Settings& settings = myOSystem->settings(); if(logging) @@ -133,6 +117,9 @@ bool StellaLIBRETRO::create(bool logging) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + console_timing = myOSystem->console().timing(); + phosphor_default = myOSystem->frameBuffer().tiaSurface().phosphorEnabled(); + if(video_phosphor == "never") setVideoPhosphor(1, video_phosphor_blend); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/libretro/libretro.cxx b/src/libretro/libretro.cxx index 6e78d8012..b786a5331 100644 --- a/src/libretro/libretro.cxx +++ b/src/libretro/libretro.cxx @@ -447,12 +447,12 @@ void retro_set_environment(retro_environment_t cb) struct retro_variable variables[] = { // Adding more variables and rearranging them is safe. { "stella_console", "Console display; auto|ntsc|pal|secam|ntsc50|pal60|secam60" }, + { "stella_palette", "Palette colors; standard|z26" }, { "stella_filter", "TV effects; disabled|composite|s-video|rgb|badly adjusted" }, - { "stella_ntsc_aspect", "NTSC aspect %; par|86|87|88|89|90|91|92|93|94|95|96|97|98|99|100|101|102|103|104|105|106|107|108|109|110|111|112|113|114|115|116|117|118|119|120|121|122|123|124|125|50|75|76|77|78|79|80|81|82|83|84|85" }, - { "stella_pal_aspect", "PAL aspect %; par|104|105|106|107|108|109|110|111|112|113|114|115|116|117|118|119|120|121|122|123|124|125|50|75|76|77|78|79|80|81|82|83|84|85|86|87|88|89|90|91|92|93|94|95|96|97|98|99|100|101|102|103" }, + { "stella_ntsc_aspect", "NTSC aspect %; par|100|101|102|103|104|105|106|107|108|109|110|111|112|113|114|115|116|117|118|119|120|121|122|123|124|125|50|75|76|77|78|79|80|81|82|83|84|85|86|87|88|89|90|91|92|93|94|95|96|97|98|99" }, + { "stella_pal_aspect", "PAL aspect %; par|100|101|102|103|104|105|106|107|108|109|110|111|112|113|114|115|116|117|118|119|120|121|122|123|124|125|50|75|76|77|78|79|80|81|82|83|84|85|86|87|88|89|90|91|92|93|94|95|96|97|98|99" }, { "stella_crop_hoverscan", "Crop horizontal overscan; disabled|enabled" }, { "stella_stereo", "Stereo sound; auto|off|on" }, - { "stella_palette", "Palette colors; standard|z26" }, { "stella_phosphor", "Phosphor mode; auto|off|on" }, { "stella_phosphor_blend", "Phosphor blend %; 60|65|70|75|80|85|90|95|100|0|5|10|15|20|25|30|35|40|45|50|55" }, { "stella_paddle_joypad_sensitivity", "Paddle joypad sensitivity; 3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|1|2" },