#include "qapple.h" #include "ui_qapple.h" #include "StdAfx.h" #include "Common.h" #include "CardManager.h" #include "Applewin.h" #include "Disk.h" #include "Harddisk.h" #include "Log.h" #include "CPU.h" #include "Frame.h" #include "Memory.h" #include "LanguageCard.h" #include "Mockingboard.h" #include "MouseInterface.h" #include "ParallelPrinter.h" #include "Video.h" #include "SoundCore.h" #include "NTSC.h" #include "SaveState.h" #include "Riff.h" #include "linux/data.h" #include "linux/benchmark.h" #include "linux/version.h" #include "linux/paddle.h" #include "emulator.h" #include "memorycontainer.h" #include "audiogenerator.h" #include "qdirectsound.h" #include "gamepadpaddle.h" #include "preferences.h" #include #include #include #include #include #include #include #include namespace { void initialiseEmulator() { #ifdef RIFF_MB RiffInitWriteFile("/tmp/Mockingboard.wav", 44100, 2); #endif g_fh = fopen("/tmp/applewin.txt", "w"); setbuf(g_fh, nullptr); LogFileOutput("Initialisation\n"); g_bFullSpeed = false; } /* In AppleWin there are 3 ways to reset the emulator * * 1) Hardware Change: it leaves the EnterMessageLoop() function * 2) ResetMachineState() * 3) CtrlReset() * * Here we implement something similar to 1) * */ void loadEmulator(QWidget * window, Emulator * emulator, const GlobalOptions & options) { ImageInitialize(); LoadConfiguration(); CheckCpu(); SetWindowTitle(); window->setWindowTitle(QString::fromStdString(g_pAppTitle)); FrameRefreshStatus(DRAW_LEDS | DRAW_BUTTON_DRIVES); ResetDefaultMachineMemTypes(); switch (options.slot0Card) { case 1: // Language Card SetExpansionMemType(CT_LanguageCard); break; case 2: // Saturn 64 SetSaturnMemorySize(Saturn128K::kMaxSaturnBanks / 2); SetExpansionMemType(CT_Saturn128K); break; case 3: // Saturn 128 SetSaturnMemorySize(Saturn128K::kMaxSaturnBanks); SetExpansionMemType(CT_Saturn128K); break; case 4: // RamWorks SetRamWorksMemorySize(options.ramWorksMemorySize); SetExpansionMemType(CT_RamWorksIII); break; } DSInit(); MB_Initialize(); MemInitialize(); VideoInitialize(); emulator->displayLogo(); g_CardMgr.GetDisk2CardMgr().Reset(); HD_Reset(); } void unloadEmulator() { CMouseInterface* pMouseCard = g_CardMgr.GetMouseCard(); if (pMouseCard) { pMouseCard->Reset(); } HD_Destroy(); PrintDestroy(); MemDestroy(); VideoDestroy(); MB_Destroy(); DSUninit(); CpuDestroy(); g_CardMgr.GetDisk2CardMgr().Destroy(); ImageDestroy(); LogDone(); RiffFinishWriteFile(); AudioGenerator::instance().stop(); QDirectSound::stop(); } qint64 emulatorTimeInMS() { const double timeInSeconds = g_nCumulativeCycles / g_fCurrentCLK6502; const qint64 timeInMS = timeInSeconds * 1000; return timeInMS; } } void FrameDrawDiskLEDS(HDC) { } void FrameDrawDiskStatus(HDC) { } void FrameRefreshStatus(int, bool) { } // MessageBox int MessageBox(HWND, const char * text, const char * caption, UINT type) { QMessageBox::StandardButtons buttons = QMessageBox::Ok; if (type & MB_YESNO) { buttons = QMessageBox::Yes | QMessageBox::No; } else if (type & MB_YESNOCANCEL) { buttons = QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel; } QMessageBox::StandardButton result = QMessageBox::information(nullptr, caption, text, buttons); switch (result) { case QMessageBox::Ok: return IDOK; case QMessageBox::Yes: return IDYES; case QMessageBox::No: return IDNO; case QMessageBox::Cancel: return IDCANCEL; default: return IDOK; } } QApple::PauseEmulator::PauseEmulator(QApple * qapple) : myQApple(qapple) { myWasRunning = myQApple->ui->actionPause->isEnabled(); if (myWasRunning) { myQApple->ui->actionPause->trigger(); } } QApple::PauseEmulator::~PauseEmulator() { if (myWasRunning) { myQApple->ui->actionStart->trigger(); } } QApple::QApple(QWidget *parent) : QMainWindow(parent), myTimerID(0), ui(new Ui::QApple) { ui->setupUi(this); ui->actionStart->setIcon(style()->standardIcon(QStyle::SP_MediaPlay)); ui->actionPause->setIcon(style()->standardIcon(QStyle::SP_MediaPause)); ui->actionReboot->setIcon(style()->standardIcon(QStyle::SP_MediaSkipBackward)); ui->actionSave_state->setIcon(style()->standardIcon(QStyle::SP_DialogSaveButton)); ui->actionLoad_state->setIcon(style()->standardIcon(QStyle::SP_DialogOpenButton)); mySaveStateLabel = new QLabel; statusBar()->addPermanentWidget(mySaveStateLabel); myPreferences = new Preferences(this); myEmulator = new Emulator(ui->mdiArea); myEmulatorWindow = ui->mdiArea->addSubWindow(myEmulator, Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowMinMaxButtonsHint); connect(AudioGenerator::instance().getAudioOutput(), SIGNAL(stateChanged(QAudio::State)), this, SLOT(on_stateChanged(QAudio::State))); readSettings(); myOptions = GlobalOptions::fromQSettings(); reloadOptions(); on_actionPause_triggered(); initialiseEmulator(); loadEmulator(myEmulatorWindow, myEmulator, myOptions); } QApple::~QApple() { delete ui; } void QApple::closeEvent(QCloseEvent * event) { stopTimer(); unloadEmulator(); QSettings settings; settings.setValue("QApple/window/geometry", saveGeometry().toBase64()); settings.setValue("QApple/window/windowState", saveState().toBase64()); settings.setValue("QApple/emulator/geometry", myEmulatorWindow->saveGeometry().toBase64()); QMainWindow::closeEvent(event); } void QApple::readSettings() { // this does not work completely in wayland // position is not restored QSettings settings; const QByteArray windowGeometry = QByteArray::fromBase64(settings.value("QApple/window/geometry").toByteArray()); const QByteArray windowState = QByteArray::fromBase64(settings.value("QApple/window/state").toByteArray()); const QByteArray emulatorGeometry = QByteArray::fromBase64(settings.value("QApple/emulator/geometry").toByteArray()); restoreGeometry(windowGeometry); restoreState(windowState); myEmulatorWindow->restoreGeometry(emulatorGeometry); } void QApple::startEmulator() { ui->actionStart->trigger(); } void QApple::on_stateChanged(QAudio::State state) { AudioGenerator::instance().stateChanged(state); } void QApple::on_timer() { AudioGenerator::instance().start(); QDirectSound::start(); if (!myElapsedTimer.isValid()) { myElapsedTimer.start(); myCpuTimeReference = emulatorTimeInMS(); } // target x ms ahead of where we are now, which is when the timer should be called again const qint64 target = myElapsedTimer.elapsed() + myOptions.msGap; const qint64 current = emulatorTimeInMS() - myCpuTimeReference; if (current > target) { // we got ahead of the timer by a lot // just check if we got something to write AudioGenerator::instance().writeAudio(); QDirectSound::writeAudio(); // wait next call return; } const qint64 maximumToRum = 10 * myOptions.msGap; // just to avoid crazy times (e.g. debugging) const qint64 toRun = std::min(target - current, maximumToRum); const double fUsecPerSec = 1.e6; const qint64 nExecutionPeriodUsec = 1000 * toRun; const double fExecutionPeriodClks = g_fCurrentCLK6502 * (double(nExecutionPeriodUsec) / fUsecPerSec); const DWORD uCyclesToExecute = fExecutionPeriodClks; const bool bVideoUpdate = true; int count = 0; const UINT dwClksPerFrame = NTSC_GetCyclesPerFrame(); do { const DWORD uActualCyclesExecuted = CpuExecute(uCyclesToExecute, bVideoUpdate); g_dwCyclesThisFrame += uActualCyclesExecuted; g_CardMgr.GetDisk2CardMgr().UpdateDriveState(uActualCyclesExecuted); MB_PeriodicUpdate(uActualCyclesExecuted); // in case we run more than 1 frame g_dwCyclesThisFrame = g_dwCyclesThisFrame % dwClksPerFrame; ++count; } while (g_CardMgr.GetDisk2CardMgr().IsConditionForFullSpeed() && (myElapsedTimer.elapsed() < target + myOptions.msFullSpeed)); // just repaint each time, to make it simpler // we run @ 60 fps anyway myEmulator->updateVideo(); if (count > 1) // 1 is the non-full speed case { restartTimeCounters(); } else { AudioGenerator::instance().writeAudio(); QDirectSound::writeAudio(); } } void QApple::stopTimer() { if (myTimerID) { restartTimeCounters(); killTimer(myTimerID); myTimerID = 0; } } void QApple::restartTimeCounters() { // let them restart next time AudioGenerator::instance().stop(); QDirectSound::stop(); myElapsedTimer.invalidate(); } void QApple::on_actionStart_triggered() { // always restart with the same timer gap that was last used myTimerID = startTimer(myOptions.msGap, Qt::PreciseTimer); ui->actionPause->setEnabled(true); ui->actionStart->setEnabled(false); restartTimeCounters(); } void QApple::on_actionPause_triggered() { stopTimer(); ui->actionPause->setEnabled(false); ui->actionStart->setEnabled(true); } void QApple::on_actionX1_triggered() { myEmulator->setZoom(myEmulatorWindow, 1); } void QApple::on_actionX2_triggered() { myEmulator->setZoom(myEmulatorWindow, 2); } void QApple::on_action4_3_triggered() { myEmulator->set43AspectRatio(myEmulatorWindow); } void QApple::on_actionReboot_triggered() { PauseEmulator pause(this); emit endEmulator(); mySaveStateLabel->clear(); unloadEmulator(); loadEmulator(myEmulatorWindow, myEmulator, myOptions); myEmulatorWindow->setWindowTitle(QString::fromStdString(g_pAppTitle)); myEmulator->updateVideo(); } void QApple::on_actionBenchmark_triggered() { // call repaint as we really want to for a paintEvent() so we can time it properly // if video is based on OpenGLWidget, this is not enough though, // and benchmark results are bad. VideoBenchmark([this]() { myEmulator->redrawScreen(); }, [this]() { myEmulator->refreshScreen(); }); on_actionReboot_triggered(); } void QApple::timerEvent(QTimerEvent *) { on_timer(); } void QApple::on_actionMemory_triggered() { MemoryContainer * container = new MemoryContainer(ui->mdiArea); QMdiSubWindow * window = ui->mdiArea->addSubWindow(container); // need to close as it points to old memory connect(this, SIGNAL(endEmulator()), window, SLOT(close())); window->setWindowTitle("Memory viewer"); window->show(); } void QApple::on_actionOptions_triggered() { // this is to overcome an issue in GNOME where the open file dialog gets lost // if the emulator is running // at times it can be found in a separate desktop, or it magically reappears // but often it forces to terminate the emulator PauseEmulator pause(this); PreferenceData currentData; getAppleWinPreferences(currentData); currentData.options = myOptions; QSettings settings; // the function will "modify" it myPreferences->setup(currentData, settings); if (myPreferences->exec()) { const PreferenceData newData = myPreferences->getData(); setAppleWinPreferences(currentData, newData); myOptions.setData(newData.options); reloadOptions(); } } void QApple::reloadOptions() { SetWindowTitle(); myEmulatorWindow->setWindowTitle(QString::fromStdString(g_pAppTitle)); Paddle::instance() = GamepadPaddle::fromName(myOptions.gamepadName); AudioGenerator::instance().setOptions(myOptions.audioLatency, myOptions.silenceDelay, myOptions.volume); QDirectSound::setOptions(myOptions.audioLatency, myOptions.silenceDelay, myOptions.volume); } void QApple::on_actionSave_state_triggered() { Snapshot_SaveState(); } void QApple::on_actionLoad_state_triggered() { PauseEmulator pause(this); emit endEmulator(); Snapshot_LoadState(); SetWindowTitle(); myEmulatorWindow->setWindowTitle(QString::fromStdString(g_pAppTitle)); const std::string & filename = Snapshot_GetFilename(); QString message = QString("State file: %1").arg(QString::fromStdString(filename)); mySaveStateLabel->setText(message); myEmulator->updateVideo(); } void QApple::on_actionAbout_Qt_triggered() { QMessageBox::aboutQt(this); } void QApple::on_actionAbout_triggered() { QString qversion = QString::fromStdString(getVersion()); QString message = QString("Apple ][ emulator\n\nBased on AppleWin %1\n").arg(qversion); QMessageBox::about(this, QApplication::applicationName(), message); } QString getImageFilename(const GlobalOptions & options) { static size_t counter = 0; const size_t maximum = 10000; while (counter < maximum) { const QString filename = options.screenshotTemplate.arg(counter, 5, 10, QChar('0')); if (!QFile(filename).exists()) { return filename; } ++counter; } return QString(); } void QApple::on_actionScreenshot_triggered() { const QString filename = getImageFilename(myOptions); if (filename.isEmpty()) { QMessageBox::warning(this, "Screenshot", "Cannot determine the screenshot filename."); } else { const bool ok = myEmulator->saveScreen(filename); if (!ok) { const QString message = QString::fromUtf8("Cannot save screenshot to %1").arg(filename); QMessageBox::warning(this, "Screenshot", message); } } } void QApple::on_actionSwap_disks_triggered() { PauseEmulator pause(this); if (g_CardMgr.QuerySlot(SLOT6) == CT_Disk2) { dynamic_cast(g_CardMgr.GetObj(SLOT6))->DriveSwap(); } } void QApple::on_actionLoad_state_from_triggered() { PauseEmulator pause(this); QFileDialog stateFileDialog(this); stateFileDialog.setFileMode(QFileDialog::AnyFile); if (stateFileDialog.exec()) { QStringList files = stateFileDialog.selectedFiles(); if (files.size() == 1) { const QString & filename = files[0]; loadStateFile(filename); } } } void QApple::on_actionNext_video_mode_triggered() { g_eVideoType++; if (g_eVideoType >= NUM_VIDEO_MODES) g_eVideoType = 0; SetWindowTitle(); myEmulatorWindow->setWindowTitle(QString::fromStdString(g_pAppTitle)); Config_Save_Video(); VideoReinitialize(); VideoRedrawScreen(); } void QApple::loadStateFile(const QString & filename) { const QFileInfo file(filename); const QString path = file.absoluteDir().canonicalPath(); // this is useful as snapshots from the test // have relative disk location SetCurrentImageDir(path.toStdString().c_str()); Snapshot_SetFilename(filename.toStdString().c_str()); ui->actionLoad_state->trigger(); }