Persist peripheral state between program invocations
Adds support for serializing the set of instantiated peripherals alongside their parameters.
MODIFY
src/io/iobase.cpp
src/io/iobase.cpp
@@ -28,6 +28,7 @@ bool IOBase::setParameter(unsigned ID, const QVariant& value) {
m_parameters.at(ID).value = value;
parameterChanged(ID);
const auto postSize = size();
+ emit paramsChanged();
if (preSize != postSize) {
emit sizeChanged();

MODIFY
src/io/iobase.h
src/io/iobase.h
#include <set>
#include "binutils.h"
+#include "serializers.h"
+
+#include "VSRTL/external/cereal/include/cereal/cereal.hpp"
namespace Ripes {
@@ -28,6 +31,11 @@ struct IOParam {
bool hasRange = false;
QVariant min;
QVariant max;
+
+ template <class Archive>
+ void serialize(Archive& archive) {
+ archive(value);
+ }
};
struct IOSymbol {
@@ -105,6 +113,39 @@ public:
std::function<void(uint32_t, uint32_t, uint32_t)> memWrite;
std::function<uint32_t(uint32_t, uint32_t)> memRead;
+ unsigned iotype() const { return m_type; }
+ unsigned id() const { return m_id; }
+ void setID(unsigned id) {
+ unclaimPeripheralId(m_type, m_id);
+ m_id = claimPeripheralId(m_type, id);
+ }
+
+ /**
+ * @brief serializedUniqueID
+ * @returns a unique string identifying the type and ID of this peripheral, used during serialization of peripheral
+ * state.
+ */
+ std::string serializedUniqueID() const { return std::to_string(iotype()) + "_" + std::to_string(id()); }
+
+ template <class Archive>
+ void serialize(Archive& archive) {
+ int parameters = m_parameters.size();
+ archive(cereal::make_nvp("nparameters", parameters));
+
+ // Serialize a copy of m_parameters to avoid the real instance being overridden (we want to keep parameter name,
+ // bounds, etc.).
+ auto paramCopy = m_parameters;
+ archive(cereal::make_nvp("parameters", paramCopy));
+ for (const auto& p : paramCopy) {
+ m_parameters[p.first].value = p.second.value;
+ }
+
+ // Invalidate all parameters
+ for (int i = 0; i < parameters; i++) {
+ parameterChanged(i);
+ }
+ }
+
signals:
/**
* @brief regMapChanged
@@ -132,6 +173,12 @@ signals:
*/
void aboutToDelete(std::atomic<bool>& ok);
+ /**
+ * @brief paramsChanged
+ * Emitted after a parameter has been changed _and_ recognized by the IOBase component.
+ */
+ void paramsChanged();
+
protected:
/**
* @brief unregister
@@ -148,21 +195,38 @@ protected:
unsigned m_id;
static std::map<unsigned, std::set<unsigned>> s_peripheralIDs;
- static unsigned claimPeripheralId(const unsigned& ioType) {
- auto& currentIDs = s_peripheralIDs[ioType];
-
- // Just scan linearly from 0 and up until a free ID is found. Not optimal, but # of peripherals is small so no
- // reason to overengineer.
+ static unsigned claimPeripheralId(const unsigned& ioType, int forcedID = -1) {
unsigned id = 0;
+ auto& currentIDs = s_peripheralIDs[ioType];
+ if (forcedID != -1) {
+ id = forcedID;
+ } else {
+ // Just scan linearly from 0 and up until a free ID is found. Not optimal, but # of peripherals is small so
+ // no reason to overengineer.
while (currentIDs.count(id) != 0) {
id++;
}
+ }
+
+ if (currentIDs.count(forcedID) != 0) {
+ throw std::runtime_error("Trying to claim a peripheral ID already claimed");
+ }
+
currentIDs.insert(id);
+
return id;
}
+
static void unclaimPeripheralId(const unsigned& ioType, unsigned id) {
- Q_ASSERT(s_peripheralIDs[ioType].count(id) > 0);
- s_peripheralIDs[ioType].erase(id);
+ auto& currentIDs = s_peripheralIDs[ioType];
+ auto it = currentIDs.find(id);
+ Q_ASSERT(it != currentIDs.end());
+
+ if (it == currentIDs.end()) {
+ throw std::runtime_error("Trying to remove a non-claimed peripheral ID");
+ }
+
+ currentIDs.erase(it);
}
private:

MODIFY
src/io/iomanager.cpp
src/io/iomanager.cpp
#include "ripessettings.h"
#include <memory>
+#include <ostream>
#include "VSRTL/core/vsrtl_sparsearray.h"
@@ -14,6 +15,7 @@ IOManager::IOManager() : QObject(nullptr) {
connect(ProcessorHandler::get(), &ProcessorHandler::processorChanged, this,
&IOManager::refreshAllPeriphsToProcessor);
connect(ProcessorHandler::get(), &ProcessorHandler::reqProcessorReset, this, &IOManager::refreshMemoryMap);
+
refreshMemoryMap();
}

MODIFY
src/io/iomanager.h
src/io/iomanager.h
namespace Ripes {
+struct PeripheralID {
+ unsigned typeId;
+ unsigned uniqueId;
+ template <class Archive>
+ void serialize(Archive& archive) {
+ archive(typeId);
+ archive(uniqueId);
+ }
+};
+using PeriphIDs = std::vector<PeripheralID>;
+
struct MemoryMapEntry {
uint32_t startAddr;
unsigned size;

MODIFY
src/io/ioregistry.h
src/io/ioregistry.h
namespace Ripes {
-enum IOType { LED_MATRIX, SWITCHES, DPAD };
+enum IOType { LED_MATRIX, SWITCHES, DPAD, NPERIPHERALS };
template <typename T>
IOBase* createIO(QWidget* parent) {

MODIFY
src/iotab.cpp
src/iotab.cpp
#include "io/memorymapmodel.h"
#include "processorhandler.h"
+#include "ripessettings.h"
#include "ioperipheraltab.h"
+#include "VSRTL/external/cereal/include/cereal/archives/json.hpp"
+#include "VSRTL/external/cereal/include/cereal/types/map.hpp"
+#include "VSRTL/external/cereal/include/cereal/types/vector.hpp"
+
namespace Ripes {
IOTab::IOTab(QToolBar* toolbar, QWidget* parent) : RipesTab(toolbar, parent), m_ui(new Ui::IOTab) {
@@ -64,14 +69,24 @@ IOTab::IOTab(QToolBar* toolbar, QWidget* parent) : RipesTab(toolbar, parent), m_
connect(&IOManager::get(), &IOManager::peripheralRemoved, this, &IOTab::removePeripheral);
connect(m_ui->peripheralsTab, &QTabWidget::currentChanged, this, &IOTab::setPeripheralMDIWindowActive);
+
+ // Store peripheral state before exiting the program
+ connect(RipesSettings::getObserver(RIPES_GLOBALSIGNAL_QUIT), &SettingObserver::modified, this,
+ &IOTab::storePeripheralState);
+
+ // Reload peripheral state from the last graceful exit of the program
+ loadPeripheralState();
}
-void IOTab::createPeripheral(IOType type) {
+IOBase* IOTab::createPeripheral(IOType type, int forcedID) {
auto* peripheral = IOManager::get().createPeripheral(type);
+ if (forcedID != -1) {
+ peripheral->setID(forcedID);
+ }
// Create tab for peripheral
auto* peripheralTab = new IOPeripheralTab(this, peripheral);
m_ui->peripheralsTab->addTab(peripheralTab, peripheral->name());
- m_ioTabs[peripheral] = peripheralTab;
+ m_periphToTab[peripheral] = peripheralTab;
// It seems excessive to create a QMainWindow for each peripheral but it seems like the only way to mix MDI
// behaviour + dockable widgets that are able to pop out to a separate window
@@ -87,13 +102,74 @@ void IOTab::createPeripheral(IOType type) {
peripheral->setFocus();
m_subWindows[peripheralTab] = mdiw;
+ return peripheral;
+}
+
+void IOTab::loadPeripheralState() {
+ auto settings = RipesSettings::value(RIPES_SETTING_PERIPHERAL_SETTINGS).toString().toStdString();
+ if (settings.empty()) {
+ return;
+ }
+
+ std::istringstream in(settings);
+ try {
+ cereal::JSONInputArchive archive(in);
+
+ // Following the serialization order of storePeripheralState(), we first load the peripheral types and unique
+ // IDs, instantiate peripherals, and then load the peripheral settings.
+ PeriphIDs ids;
+ try {
+ archive(cereal::make_nvp("periphIDs", ids));
+ for (const auto& id : ids) {
+ if (id.typeId < NPERIPHERALS) {
+ auto* periph = createPeripheral(static_cast<IOType>(id.typeId), id.uniqueId);
+ }
+ }
+
+ for (const auto& periph : m_periphToTab) {
+ IOBase* periphPtr = dynamic_cast<IOBase*>(periph.first);
+ archive(cereal::make_nvp(periphPtr->serializedUniqueID(), *periphPtr));
+ }
+ } catch (cereal::Exception e) {
+ /// @todo: build an error report
+ }
+ } catch (...) {
+ // Could not load peripherals from settings...
+ return;
+ }
+}
+
+void IOTab::storePeripheralState() {
+ std::ostringstream out;
+ {
+ cereal::JSONOutputArchive archive(out);
+
+ // First, serialize the type of each peripheral, and then the peripheral itself
+ PeriphIDs ids;
+ for (const auto& periph : m_periphToTab) {
+ IOBase* periphPtr = dynamic_cast<IOBase*>(periph.first);
+ assert(periphPtr);
+ ids.push_back({periphPtr->iotype(), periphPtr->id()});
+ }
+
+ try {
+ archive(cereal::make_nvp("periphIDs", ids));
+ for (const auto& periph : m_periphToTab) {
+ IOBase* periphPtr = dynamic_cast<IOBase*>(periph.first);
+ archive(cereal::make_nvp(periphPtr->serializedUniqueID(), *periphPtr));
+ }
+ } catch (cereal::Exception e) {
+ /// @todo: build an error report
+ }
+ }
+ RipesSettings::setValue(RIPES_SETTING_PERIPHERAL_SETTINGS, QString::fromStdString(out.str()));
}
void IOTab::setPeripheralTabActive(IOBase* peripheral) {
if (peripheral == nullptr) {
return;
}
- m_ui->peripheralsTab->setCurrentWidget(m_ioTabs.at(peripheral));
+ m_ui->peripheralsTab->setCurrentWidget(m_periphToTab.at(peripheral));
}
void IOTab::setPeripheralMDIWindowActive(int tabIndex) {
@@ -111,11 +187,11 @@ void IOTab::setPeripheralMDIWindowActive(int tabIndex) {
}
void IOTab::removePeripheral(QObject* peripheral) {
- auto* tab = m_ioTabs.at(peripheral);
+ auto* tab = m_periphToTab.at(peripheral);
Q_ASSERT(m_subWindows.count(tab) != 0);
m_subWindows.erase(tab);
- m_ioTabs.at(peripheral)->deleteLater();
- m_ioTabs.erase(peripheral);
+ m_periphToTab.at(peripheral)->deleteLater();
+ m_periphToTab.erase(peripheral);
}
void IOTab::tile() {

MODIFY
src/iotab.h
src/iotab.h
@@ -36,7 +36,17 @@ private slots:
void setPeripheralMDIWindowActive(int tabIndex);
private:
- void createPeripheral(IOType type);
+ IOBase* createPeripheral(IOType type, int forcedID = -1);
+
+ /**
+ * @brief Peripheral serialization
+ * To make peripherals persist between invocations of the program, we serialize the state of all instantiated
+ * peripherals into a settings variable.
+ * Each peripheral is uniquely identified by a key (see IOBase::serializedID()). For each peripheral, we then store
+ * the values of its parameters (see IOParam).
+ */
+ void loadPeripheralState();
+ void storePeripheralState();
void tile();
Ui::IOTab* m_ui = nullptr;
@@ -48,7 +58,7 @@ private:
* IOBase* given that the association is used during emission of the QObject::destroyed signal. In this case, the
* IOBase* subclass has already been destroyed.
*/
- std::unordered_map<QObject*, QWidget*> m_ioTabs;
+ std::unordered_map<QObject*, QWidget*> m_periphToTab;
/**
* @brief m_ioTabs

MODIFY
src/iotab.ui
src/iotab.ui
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
+ <property name="childrenCollapsible">
+ <bool>false</bool>
+ </property>
<widget class="QTableWidget" name="peripheralsTable">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">

MODIFY
src/mainwindow.cpp
src/mainwindow.cpp
@@ -268,6 +268,9 @@ void MainWindow::closeEvent(QCloseEvent* event) {
saveFilesTriggered();
}
}
+
+ // Emit an observable signal to indicate that the application is about to close
+ RipesSettings::setValue(RIPES_GLOBALSIGNAL_QUIT, 0);
QMainWindow::closeEvent(event);
}

MODIFY
src/ripessettings.h
src/ripessettings.h
@@ -24,6 +24,12 @@ namespace Ripes {
#define RIPES_SETTING_PERIPHERALS_START ("peripheral_start")
+#define RIPES_SETTING_PERIPHERAL_SETTINGS ("peripheral_settings")
+
+// This is not really a setting, but instead a method to leverage the static observer objects that are generated for a
+// setting. Used for other objects to hook into a signal emitted just before the application closes.
+#define RIPES_GLOBALSIGNAL_QUIT ("sig_quit")
+
// Program state preserving settings
#define RIPES_SETTING_SETTING_TAB ("settings_tab")
#define RIPES_SETTING_VIEW_ZOOM ("view_zoom")
@@ -52,8 +58,10 @@ const static std::map<QString, QVariant> s_defaultSettings = {
{RIPES_SETTING_PERIPHERALS_START, static_cast<unsigned>(0xF0000000)},
// Program state preserving settings
+ {RIPES_GLOBALSIGNAL_QUIT, 0},
{RIPES_SETTING_SETTING_TAB, 0},
{RIPES_SETTING_VIEW_ZOOM, 250},
+ {RIPES_SETTING_PERIPHERAL_SETTINGS, ""},
{RIPES_SETTING_PROCESSOR_ID, QVariant() /* Let processorhandler define default */},
{RIPES_SETTING_PROCESSOR_LAYOUT_ID, 0},
{RIPES_SETTING_FOLLOW_EXEC, "true"},

ADD
src/serializers.h
+#pragma once
+
+#include "VSRTL/external/cereal/include/cereal/cereal.hpp"
+
+#include <QDataStream>
+#include <QVariant>
+
+template <class Archive>
+void save(Archive& archive, const QVariant& v) {
+ // Seems cumbersome but only easy way to serialize a QVariant in cereal
+ QByteArray data;
+ QDataStream ds(&data, QIODevice::WriteOnly);
+ ds << v;
+ std::vector<std::byte> dataVector(reinterpret_cast<std::byte*>(std::begin(data)),
+ reinterpret_cast<std::byte*>(std::end(data)));
+ archive(dataVector);
+}
+
+template <class Archive>
+void load(Archive& archive, QVariant& v) {
+ // Seems cumbersome but only easy way to serialize a QVariant in cereal
+ std::vector<std::byte> dataVector;
+ archive(dataVector);
+ QByteArray data = QByteArray(reinterpret_cast<const char*>(dataVector.data()), dataVector.size());
+ QDataStream ds(&data, QIODevice::ReadOnly);
+ ds >> v;
+}

Persist peripheral state between program invocations

Adds support for serializing the set of instantiated peripherals alongside their parameters.