From f8ec4aeea5b9758e89ce4df4ec69b72d07ea1da9 Mon Sep 17 00:00:00 2001 From: Chris Greening Date: Mon, 25 Nov 2024 20:07:11 +0000 Subject: [PATCH] Time travel - WIP --- firmware/src/AudioOutput/BuzzerOuput.cpp | 2 +- firmware/src/AudioOutput/BuzzerOutput.h | 4 +- firmware/src/Emulator/snaps.cpp | 28 ++-- firmware/src/Emulator/spectrum.cpp | 2 +- firmware/src/Emulator/spectrum.h | 37 ++++-- firmware/src/Emulator/z80/z80.cpp | 9 +- .../src/Screens/EmulatorScreen/GameLoader.cpp | 2 +- .../src/Screens/EmulatorScreen/Machine.cpp | 11 +- firmware/src/Screens/EmulatorScreen/Machine.h | 124 ++++++++++++++++++ 9 files changed, 180 insertions(+), 39 deletions(-) diff --git a/firmware/src/AudioOutput/BuzzerOuput.cpp b/firmware/src/AudioOutput/BuzzerOuput.cpp index 6ade152..7fed4b6 100644 --- a/firmware/src/AudioOutput/BuzzerOuput.cpp +++ b/firmware/src/AudioOutput/BuzzerOuput.cpp @@ -86,7 +86,7 @@ bool IRAM_ATTR BuzzerOutput::onTimer() uint16_t sample = mBuffer[mCurrentIndex]; if (micValue) { - sample = std::min(255, sample + 5); + sample = std::min(255, sample + 50); } // go up to 9 bits resolution sample = sample << 1; diff --git a/firmware/src/AudioOutput/BuzzerOutput.h b/firmware/src/AudioOutput/BuzzerOutput.h index d00829a..b781ebf 100644 --- a/firmware/src/AudioOutput/BuzzerOutput.h +++ b/firmware/src/AudioOutput/BuzzerOutput.h @@ -29,8 +29,8 @@ class BuzzerOutput : public AudioOutput BuzzerOutput(gpio_num_t buzzerPin) : AudioOutput() { // set up the ADC - // adc1_config_width(ADC_WIDTH_BIT_12); - // adc1_config_channel_atten(ADC1_CHANNEL_4, ADC_ATTEN_DB_11); + adc1_config_width(ADC_WIDTH_BIT_12); + adc1_config_channel_atten(ADC1_CHANNEL_4, ADC_ATTEN_DB_11); mBuzzerPin = buzzerPin; mBufferSemaphore = xSemaphoreCreateBinary(); xSemaphoreGive(mBufferSemaphore); diff --git a/firmware/src/Emulator/snaps.cpp b/firmware/src/Emulator/snaps.cpp index 330b89b..f61ca0d 100644 --- a/firmware/src/Emulator/snaps.cpp +++ b/firmware/src/Emulator/snaps.cpp @@ -297,7 +297,7 @@ bool loadZ80Version2or3(ZXSpectrum *speccy, uint8_t *buffer, int version, FILE * int dataOffset = 30 + 2 + buffer[30]; fseek(fp, dataOffset, SEEK_SET); - uint8_t *pageMap[16] = {0}; + MemoryPage *pageMap[16] = {0}; if (hwmodel == SPECMDL_48K) { pageMap[4] = speccy->mem.banks[2]; @@ -335,7 +335,7 @@ bool loadZ80Version2or3(ZXSpectrum *speccy, uint8_t *buffer, int version, FILE * fread(pageMap[page], actualLength, 1, fp); } else { printf("Decompressing page %d\n", page); - decompressZ80BlockV2orV3(fp, length, pageMap[page], 0x4000); + decompressZ80BlockV2orV3(fp, length, pageMap[page]->data, 0x4000); } } speccy->z80Regs->PC.B.l = buffer[32]; @@ -461,20 +461,20 @@ bool saveZ80(ZXSpectrum *speccy, const char *filename) // Map pages based on 48K or 128K model if (speccy->hwopt.hw_model == SPECMDL_48K) { - pageMap[4] = speccy->mem.banks[2]; - pageMap[5] = speccy->mem.banks[0]; - pageMap[8] = speccy->mem.banks[5]; + pageMap[4] = speccy->mem.banks[2]->data; + pageMap[5] = speccy->mem.banks[0]->data; + pageMap[8] = speccy->mem.banks[5]->data; } else if (speccy->hwopt.hw_model == SPECMDL_128K) { - pageMap[3] = speccy->mem.banks[0]; - pageMap[4] = speccy->mem.banks[1]; - pageMap[5] = speccy->mem.banks[2]; - pageMap[6] = speccy->mem.banks[3]; - pageMap[7] = speccy->mem.banks[4]; - pageMap[8] = speccy->mem.banks[5]; - pageMap[9] = speccy->mem.banks[6]; - pageMap[10] = speccy->mem.banks[7]; + pageMap[3] = speccy->mem.banks[0]->data; + pageMap[4] = speccy->mem.banks[1]->data; + pageMap[5] = speccy->mem.banks[2]->data; + pageMap[6] = speccy->mem.banks[3]->data; + pageMap[7] = speccy->mem.banks[4]->data; + pageMap[8] = speccy->mem.banks[5]->data; + pageMap[9] = speccy->mem.banks[6]->data; + pageMap[10] = speccy->mem.banks[7]->data; } for (int page = 0; page < 16; ++page) @@ -604,7 +604,7 @@ bool LoadSNA(ZXSpectrum *speccy, const char *filename) } else { - fread(speccy->mem.banks[i], 0x4000, 1, fp); + fread(speccy->mem.banks[i]->data, 0x4000, 1, fp); } } } diff --git a/firmware/src/Emulator/spectrum.cpp b/firmware/src/Emulator/spectrum.cpp index be91c43..d076b7d 100644 --- a/firmware/src/Emulator/spectrum.cpp +++ b/firmware/src/Emulator/spectrum.cpp @@ -60,7 +60,7 @@ void ZXSpectrum::reset() int ZXSpectrum::runForFrame(AudioOutput *audioOutput, FILE *audioFile) { uint8_t audioBuffer[312]; - uint8_t *attrBase = mem.currentScreen + 0x1800; + uint8_t *attrBase = mem.currentScreen->data + 0x1800; int c = 0; // Each line should be 224 tstates long... // And a complete frame is (64+192+56)*224=69888 tstates long diff --git a/firmware/src/Emulator/spectrum.h b/firmware/src/Emulator/spectrum.h index 561fa5a..7052960 100644 --- a/firmware/src/Emulator/spectrum.h +++ b/firmware/src/Emulator/spectrum.h @@ -57,29 +57,39 @@ typedef struct uint8_t SoundBits; } tipo_hwopt; +class MemoryPage { +public: + bool isDirty; + uint8_t *data; + MemoryPage() { + isDirty = false; + data = (uint8_t *) malloc(0x4000); + memset(data, 0, 0x4000); + } +}; + class Memory { public: uint8_t hwBank = 0; - uint8_t *rom[2]; - uint8_t *banks[8]; - uint8_t *currentScreen; - uint8_t *mappedMemory[4]; + MemoryPage *rom[2]; + MemoryPage *banks[8]; + // track is a memory bank is dirty + MemoryPage *currentScreen; + MemoryPage *mappedMemory[4]; Memory() { // allocate space for the rom for (int i = 0; i < 2; i++) { - rom[i] = (uint8_t *)malloc(0x4000); - if (rom[i] == 0) { + rom[i] = new MemoryPage(); + if (rom[i] == nullptr || rom[i]->data == nullptr) { printf("Failed to allocate ROM"); } - memset(rom[i], 0, 0x4000); } // allocate space for the memory banks for (int i = 0; i < 8; i++) { - banks[i] = (uint8_t *)malloc(0x4000); - if (banks[i] == 0) { + banks[i] = new MemoryPage(); + if (banks[i] == nullptr || banks[i]->data == nullptr) { printf("Failed to allocate RAM"); } - memset(banks[i], 0, 0x4000); } // wire up the default memory configuration - this will work for the 48k model and is the default for the 128k model mappedMemory[0] = rom[0]; @@ -105,7 +115,7 @@ class Memory { inline uint8_t peek(int address) { int memoryBank = address >> 14; int bankAddress = address & 0x3fff; - return mappedMemory[memoryBank][bankAddress]; + return mappedMemory[memoryBank]->data[bankAddress]; } inline void poke(int address, uint8_t value) { int memoryBank = address >> 14; @@ -113,7 +123,8 @@ class Memory { if (memoryBank == 0) { // ignore writes to rom } else { - mappedMemory[memoryBank][bankAddress] = value; + mappedMemory[memoryBank]->data[bankAddress] = value; + mappedMemory[memoryBank]->isDirty = true; } } void loadRom(const uint8_t *rom_data, int rom_len) { @@ -121,7 +132,7 @@ class Memory { int romCount = rom_len / 0x4000; for (int i = 0; i < romCount; i++) { printf("Copying ROM %d\n", i); - memcpy(rom[i], rom_data + (i * 0x4000), 0x4000); + memcpy(rom[i]->data, rom_data + (i * 0x4000), 0x4000); } } }; diff --git a/firmware/src/Emulator/z80/z80.cpp b/firmware/src/Emulator/z80/z80.cpp index 982796c..83dcfa6 100644 --- a/firmware/src/Emulator/z80/z80.cpp +++ b/firmware/src/Emulator/z80/z80.cpp @@ -21,12 +21,13 @@ #include "tables.h" #include "z80.h" -#define Z80ReadMem(where) (mappedMemory[(where) >> 14][(where) & 0x3FFF]) +#define Z80ReadMem(where) (mappedMemory[(where) >> 14]->data[(where) & 0x3FFF]) #define Z80WriteMem(where, A, regs) ({ \ int memoryBank = (where) >> 14; \ if (memoryBank != 0) \ { \ - mappedMemory[memoryBank][(where) & 0x3fff] = A; \ + mappedMemory[memoryBank]->data[(where) & 0x3fff] = A; \ + mappedMemory[memoryBank]->isDirty = true; \ } \ }) #define Z80InPort(regs, port) (spectrum->z80_in(port)) @@ -118,7 +119,7 @@ uint16_t Z80Run(Z80Regs *regs, int numcycles) { ZXSpectrum *spectrum = ((ZXSpectrum *)regs->userInfo); Memory &memory = spectrum->mem; - uint8_t **mappedMemory = memory.mappedMemory; + MemoryPage **mappedMemory = memory.mappedMemory; /* opcode and temp variables */ byte opcode; eword tmpreg, ops, mread, tmpreg2; @@ -194,7 +195,7 @@ void Z80Interrupt(Z80Regs *regs, uint16_t ivec) { ZXSpectrum *spectrum = ((ZXSpectrum *)regs->userInfo); Memory &memory = spectrum->mem; - uint8_t **mappedMemory = memory.mappedMemory; + MemoryPage **mappedMemory = memory.mappedMemory; uint16_t intaddress; /* unhalt the computer */ diff --git a/firmware/src/Screens/EmulatorScreen/GameLoader.cpp b/firmware/src/Screens/EmulatorScreen/GameLoader.cpp index 47d6add..37d5b23 100644 --- a/firmware/src/Screens/EmulatorScreen/GameLoader.cpp +++ b/firmware/src/Screens/EmulatorScreen/GameLoader.cpp @@ -72,7 +72,7 @@ void GameLoader::loadTape(std::string filename) count++; if (borderPos == 312) { borderPos = 0; - renderer->triggerDraw(machine->getMachine()->mem.currentScreen, currentBorderColors); + renderer->triggerDraw(machine->getMachine()->mem.currentScreen->data, currentBorderColors); } if (count % 4000 == 0) { float machineTime = (float) listener->getTotalTicks() / 3500000.0f; diff --git a/firmware/src/Screens/EmulatorScreen/Machine.cpp b/firmware/src/Screens/EmulatorScreen/Machine.cpp index a0e0446..f286f1c 100644 --- a/firmware/src/Screens/EmulatorScreen/Machine.cpp +++ b/firmware/src/Screens/EmulatorScreen/Machine.cpp @@ -15,7 +15,7 @@ void Machine::runEmulator() { if (isRunning) { cycleCount += machine->runForFrame(audioOutput, audioFile); - renderer->triggerDraw(machine->mem.currentScreen, machine->borderColors); + renderer->triggerDraw(machine->mem.currentScreen->data, machine->borderColors); unsigned long currentTime = millis(); unsigned long elapsed = currentTime - lastTime; if (elapsed > 1000) @@ -26,6 +26,10 @@ void Machine::runEmulator() { Serial.printf("Executed at %.3FMHz cycles, frame rate=%.2f\n", cycles, fps); renderer->resetFrameCount(); cycleCount = 0; + // save the state of the machine for time travel + timeTravel->record(machine); + Serial.printf("Free heap: %d\n", ESP.getFreeHeap()); + Serial.printf("Free PSRAM: %d\n", ESP.getFreePsram()); } if (machine->romLoadingRoutineHit) { @@ -44,6 +48,7 @@ Machine::Machine(Renderer *renderer, AudioOutput *audioOutput, std::functionrunForFrame(nullptr, nullptr); } - renderer->triggerDraw(machine->mem.currentScreen, machine->borderColors); + renderer->triggerDraw(machine->mem.currentScreen->data, machine->borderColors); // TODO load screenshot... if (machine->hwopt.hw_model == SPECMDL_48K) { @@ -103,5 +108,5 @@ void Machine::startLoading() // 128K the tape loader is first in the menu tapKey(SPECKEY_ENTER); } - renderer->triggerDraw(machine->mem.currentScreen, machine->borderColors); + renderer->triggerDraw(machine->mem.currentScreen->data, machine->borderColors); } \ No newline at end of file diff --git a/firmware/src/Screens/EmulatorScreen/Machine.h b/firmware/src/Screens/EmulatorScreen/Machine.h index efeac1c..378e40d 100644 --- a/firmware/src/Screens/EmulatorScreen/Machine.h +++ b/firmware/src/Screens/EmulatorScreen/Machine.h @@ -2,7 +2,11 @@ #include "stdio.h" #include +#include +#include +#include #include "../../Emulator/spectrum.h" +#include "../../Serial.h" void runnerTask(void *pvParameter); @@ -10,6 +14,124 @@ class Renderer; class ZXSpectrum; class AudioOutput; +// handles the time travel functionality - this captures the state of the machine at a point in time +struct MemoryBank { + // the index + uint8_t index = 0; + // the memory bank + uint8_t *data = nullptr; +}; + +struct TimeTravelInstant { + std::vector memoryBanks; + Z80Regs z80Regs; + uint8_t borderColors[32] = {0}; +}; + +class TimeTravel { +private: + // list of time travel instants that have been recorded + std::deque timeTravelInstants; + // pool of memory banks that we can reuse + std::list memoryBanks; + bool ensureMemoryBanks(size_t required) { + while(memoryBanks.size() < required && timeTravelInstants.size() > 0) { + TimeTravelInstant *oldestInstant = timeTravelInstants.front(); + timeTravelInstants.pop_front(); + for (MemoryBank *memoryBank : oldestInstant->memoryBanks) { + memoryBanks.push_back(memoryBank); + } + delete oldestInstant; + } + return memoryBanks.size() >= required; + } +public: + TimeTravel() { + // allocate some memory banks + for (int i = 0; i < 240; i++) { + MemoryBank *memoryBank = new MemoryBank(); + if (!memoryBank) { + Serial.println("Could not allocate memory bank"); + return; + } + memoryBank->data = (uint8_t *) ps_malloc(0x4000); + if (!memoryBank->data) { + Serial.println("Could not allocate memory bank data"); + return; + } + memoryBanks.push_back(memoryBank); + } + } + size_t size() { + return timeTravelInstants.size(); + } + // record the current state of the machine + bool record(ZXSpectrum *machine) { + // how many memory banks do we need? + int memoryBankCount = 0; + for(int i = 0; i<8; i++) { + if (machine->mem.banks[i]->isDirty) { + memoryBankCount++; + } + } + ensureMemoryBanks(memoryBankCount); + Serial.printf("Saving %d memory banks\n", memoryBankCount); + // create a new time travel instant + TimeTravelInstant *instant = new TimeTravelInstant(); + // copy the memory banks + for(int i = 0; i<8; i++) { + if (machine->mem.banks[i]->isDirty) { + MemoryBank *memoryBank = memoryBanks.front(); + memoryBanks.pop_front(); + memoryBank->index = i; + memcpy(memoryBank->data, machine->mem.banks[i]->data, 0x4000); + machine->mem.banks[i]->isDirty = false; + instant->memoryBanks.push_back(memoryBank); + } + } + // copy the z80 registers + memcpy(&instant->z80Regs, machine->z80Regs, sizeof(Z80Regs)); + // copy the border colors + memcpy(instant->borderColors, machine->borderColors, 32); + // add the instant to the list + timeTravelInstants.push_back(instant); + // if we have more than 30 seconds of time travel, remove the oldest instant + if (timeTravelInstants.size() > 30) { + TimeTravelInstant *oldestInstant = timeTravelInstants.front(); + timeTravelInstants.pop_front(); + for (MemoryBank *memoryBank : oldestInstant->memoryBanks) { + memoryBanks.push_back(memoryBank); + } + delete oldestInstant; + } + Serial.printf("Recorded time travel instant %d\n", timeTravelInstants.size()); + return true; + } + // rewind the machine to a previous state + void rewind(ZXSpectrum *machine, int index) { + TimeTravelInstant *instant = timeTravelInstants[index]; + // copy the memory banks + for (MemoryBank *memoryBank : instant->memoryBanks) { + memcpy(machine->mem.mappedMemory[memoryBank->index], memoryBank->data, 0x4000); + } + // copy the z80 registers + memcpy(machine->z80Regs, &instant->z80Regs, sizeof(Z80Regs)); + // copy the border colors + memcpy(machine->borderColors, instant->borderColors, 32); + } + // remove everything from this point in time + void reset(int index) { + while(timeTravelInstants.size() > index) { + TimeTravelInstant *instant = timeTravelInstants.back(); + timeTravelInstants.pop_back(); + for (MemoryBank *memoryBank : instant->memoryBanks) { + memoryBanks.push_back(memoryBank); + } + delete instant; + } + } +}; + class Machine { private: // the actual machine @@ -27,6 +149,8 @@ class Machine { FILE *audioFile = nullptr; // keeps track of how many tstates we've run uint32_t cycleCount = 0; + // time travel + TimeTravel *timeTravel; // callback for when rom loading routine is hit std::function romLoadingRoutineHitCallback; public: