From 1b5a14d05b13136a3fb9cb846646b7f2190d8b26 Mon Sep 17 00:00:00 2001 From: Chris Greening Date: Thu, 28 Nov 2024 16:02:38 +0000 Subject: [PATCH] implements time travel --- firmware/src/Emulator/keyboard_defs.h | 3 + firmware/src/Emulator/spectrum.cpp | 10 +- firmware/src/Screens/EmulatorScreen.cpp | 60 ++++++++- firmware/src/Screens/EmulatorScreen.h | 3 + firmware/src/Screens/EmulatorScreen/Machine.h | 45 ++++++- .../src/Screens/EmulatorScreen/Renderer.h | 6 + firmware/src/Screens/NavigationStack.h | 9 +- firmware/src/TFT/TFTDisplay.cpp | 12 ++ firmware/src/TFT/TFTDisplay.h | 122 +++++++++++++++--- firmware/src/main.cpp | 26 ++-- 10 files changed, 256 insertions(+), 40 deletions(-) diff --git a/firmware/src/Emulator/keyboard_defs.h b/firmware/src/Emulator/keyboard_defs.h index 31e3086..0426b64 100644 --- a/firmware/src/Emulator/keyboard_defs.h +++ b/firmware/src/Emulator/keyboard_defs.h @@ -45,6 +45,9 @@ enum SpecKeys SPECKEY_M, SPECKEY_SYMB, SPECKEY_SPACE, + // indicates that everything above is not a normal key + SPECKEY_MAX_NORMAL, + // joystick keys JOYK_UP, JOYK_DOWN, JOYK_LEFT, diff --git a/firmware/src/Emulator/spectrum.cpp b/firmware/src/Emulator/spectrum.cpp index e29c933..1187817 100644 --- a/firmware/src/Emulator/spectrum.cpp +++ b/firmware/src/Emulator/spectrum.cpp @@ -152,10 +152,12 @@ void ZXSpectrum::updateKey(SpecKeys key, uint8_t state) kempston_port &= 0b11101111; break; default: - if (state == 1) - speckey[key2specy[0][key]] &= key2specy[1][key]; - else - speckey[key2specy[0][key]] |= ((key2specy[1][key]) ^ 0xFF); + if (key < SPECKEY_MAX_NORMAL) { + if (state == 1) + speckey[key2specy[0][key]] &= key2specy[1][key]; + else + speckey[key2specy[0][key]] |= ((key2specy[1][key]) ^ 0xFF); + } break; } } diff --git a/firmware/src/Screens/EmulatorScreen.cpp b/firmware/src/Screens/EmulatorScreen.cpp index 74146a7..fce0143 100644 --- a/firmware/src/Screens/EmulatorScreen.cpp +++ b/firmware/src/Screens/EmulatorScreen.cpp @@ -115,7 +115,45 @@ void EmulatorScreen::updateKey(SpecKeys key, uint8_t state) // Serial.printf("Audio file closed\n"); // } // } - machine->updateKey(key, state); + if (!isInTimeTravelMode) + { + machine->updateKey(key, state); + } +} + +void EmulatorScreen::pressKey(SpecKeys key) +{ + if (key == SPECKEY_MENU) + { + // TODO show menu for save snapshot, load snapshot, time travel + if (!isInTimeTravelMode) + { + pause(); + isInTimeTravelMode = true; + machine->startTimeTravel(); + drawTimeTravel(); + } + // showSaveSnapshotScreen(); + } else if (isInTimeTravelMode) + { + // TODO cancel time travel + if (key == SPECKEY_ENTER) + { + machine->stopTimeTravel(); + isInTimeTravelMode = false; + resume(); + } + else + { + if (key == SPECKEY_5) { + machine->stepBack(); + } + if (key == SPECKEY_8) { + machine->stepForward(); + } + drawTimeTravel(); + } + } } void EmulatorScreen::showSaveSnapshotScreen() @@ -134,3 +172,23 @@ void EmulatorScreen::loadTape(std::string filename) machine->resume(); isLoading = false; } + +void EmulatorScreen::drawTimeTravel() { + m_tft.fillRect(0, 0, m_tft.width(), 20, TFT_BLACK); + + m_tft.loadFont(GillSans_15_vlw); + m_tft.setTextColor(TFT_WHITE, TFT_BLACK); + + // Draw the left control "<5" + m_tft.drawString("<5", 5, 0); + + // Draw the center text "Time Travel - Enter=Jump" + Point centerSize = m_tft.measureString("Time Travel - Enter=Jump"); + int centerX = (m_tft.width() - centerSize.x) / 2; + m_tft.drawString("Time Travel - Enter=Jump", centerX, 0); + + // Draw the right control "8>" + Point rightSize = m_tft.measureString("8>"); + int rightX = m_tft.width() - rightSize.x - 5; // 5 pixels from right edge + m_tft.drawString("8>", rightX, 0); +} diff --git a/firmware/src/Screens/EmulatorScreen.h b/firmware/src/Screens/EmulatorScreen.h index 054919c..f9446c3 100644 --- a/firmware/src/Screens/EmulatorScreen.h +++ b/firmware/src/Screens/EmulatorScreen.h @@ -21,9 +21,12 @@ class EmulatorScreen : public Screen IFiles *m_files; void triggerLoadTape(); bool isLoading = false; + bool isInTimeTravelMode = false; + void drawTimeTravel(); public: EmulatorScreen(TFTDisplay &tft, AudioOutput *audioOutput, IFiles *files); void updateKey(SpecKeys key, uint8_t state); + void pressKey(SpecKeys key); void run(std::string filename, models_enum model); void pause(); void resume(); diff --git a/firmware/src/Screens/EmulatorScreen/Machine.h b/firmware/src/Screens/EmulatorScreen/Machine.h index 4fbf066..d859704 100644 --- a/firmware/src/Screens/EmulatorScreen/Machine.h +++ b/firmware/src/Screens/EmulatorScreen/Machine.h @@ -5,6 +5,7 @@ #include #include #include +#include "Renderer.h" #include "../../Emulator/spectrum.h" #include "../../Serial.h" @@ -23,9 +24,14 @@ struct MemoryBank { }; struct TimeTravelInstant { + // the current memory pages + uint8_t hwBank = 0; + // the memory banks std::vector memoryBanks; + // the z80 registers Z80Regs z80Regs; - uint8_t borderColors[32] = {0}; + // the border colors + uint8_t borderColors[312] = {0}; }; class TimeTravel { @@ -92,7 +98,9 @@ class TimeTravel { // copy the z80 registers memcpy(&instant->z80Regs, machine->z80Regs, sizeof(Z80Regs)); // copy the border colors - memcpy(instant->borderColors, machine->borderColors, 32); + memcpy(instant->borderColors, machine->borderColors, 312); + // make sure the correct paging is set + instant->hwBank = machine->mem.hwBank; // add the instant to the list timeTravelInstants.push_back(instant); // if we have more than 30 seconds of time travel, remove the oldest instant @@ -112,12 +120,14 @@ class TimeTravel { TimeTravelInstant *instant = timeTravelInstants[index]; // copy the memory banks for (MemoryBank *memoryBank : instant->memoryBanks) { - memcpy(machine->mem.mappedMemory[memoryBank->index], memoryBank->data, 0x4000); + memcpy(machine->mem.banks[memoryBank->index]->data, memoryBank->data, 0x4000); } // copy the z80 registers memcpy(machine->z80Regs, &instant->z80Regs, sizeof(Z80Regs)); // copy the border colors - memcpy(machine->borderColors, instant->borderColors, 32); + memcpy(machine->borderColors, instant->borderColors, 312); + // make sure the correct paging is set + machine->mem.page(instant->hwBank, true); } // remove everything from this point in time void reset(int index) { @@ -151,6 +161,8 @@ class Machine { uint32_t cycleCount = 0; // time travel TimeTravel *timeTravel; + // current time travel position + int timeTravelPosition = 0; // callback for when rom loading routine is hit std::function romLoadingRoutineHitCallback; public: @@ -164,6 +176,31 @@ class Machine { void resume() { isRunning = true; } + void startTimeTravel() { + // record the current state + timeTravel->record(machine); + timeTravelPosition = timeTravel->size() - 1; + Serial.printf("Starting time travel %d\n", timeTravelPosition); + } + void stepBack() { + if (timeTravelPosition > 0) { + timeTravelPosition--; + timeTravel->rewind(machine, timeTravelPosition); + renderer->forceRedraw(machine->mem.currentScreen->data, machine->borderColors); + Serial.printf("Time travel %d\n", timeTravelPosition); + } + } + void stepForward() { + if (timeTravelPosition < timeTravel->size() - 1) { + timeTravelPosition++; + timeTravel->rewind(machine, timeTravelPosition); + renderer->forceRedraw(machine->mem.currentScreen->data, machine->borderColors); + Serial.printf("Time travel %d\n", timeTravelPosition); + } + } + void stopTimeTravel() { + timeTravel->reset(timeTravelPosition); + } ZXSpectrum *getMachine() { return machine; } diff --git a/firmware/src/Screens/EmulatorScreen/Renderer.h b/firmware/src/Screens/EmulatorScreen/Renderer.h index aee073f..0111c59 100644 --- a/firmware/src/Screens/EmulatorScreen/Renderer.h +++ b/firmware/src/Screens/EmulatorScreen/Renderer.h @@ -103,4 +103,10 @@ class Renderer { void setNeedsRedraw() { firstDraw = true; } + void forceRedraw(const uint8_t *currentScreen, const uint8_t *borderColors) { + memcpy(currentScreenBuffer, currentScreen, 6912); + memcpy(currentBorderColors, borderColors, 312); + firstDraw = true; + drawScreen(); + } }; diff --git a/firmware/src/Screens/NavigationStack.h b/firmware/src/Screens/NavigationStack.h index 39a9232..22eed3e 100644 --- a/firmware/src/Screens/NavigationStack.h +++ b/firmware/src/Screens/NavigationStack.h @@ -2,12 +2,15 @@ #include #include "Screen.h" +#include "../TFT/TFTDisplay.h" class NavigationStack { + private: + TFTDisplay *m_tft; public: std::vector stack; - NavigationStack() {} + NavigationStack(TFTDisplay *tft) : m_tft(tft) {} ~NavigationStack() {} Screen *getTop() { if (stack.size() > 0) { @@ -51,5 +54,9 @@ class NavigationStack if (top) { top->pressKey(key); } + // TODO: Add some combination of keys to save a screenshot + // if (key == SPECKEY_MENU) { + // m_tft->saveScreenshot(); + // } }; }; diff --git a/firmware/src/TFT/TFTDisplay.cpp b/firmware/src/TFT/TFTDisplay.cpp index a3cc3d2..feadc93 100644 --- a/firmware/src/TFT/TFTDisplay.cpp +++ b/firmware/src/TFT/TFTDisplay.cpp @@ -152,6 +152,8 @@ void TFTDisplay::sendTransaction(SPITransactionInfo *trans) void TFTDisplay::sendPixels(const uint16_t *data, int numPixels) { + writePixelsToFrameBuffer(data, numPixels); + int bytes = numPixels * 2; for (uint32_t i = 0; i < bytes; i += DMA_BUFFER_SIZE) { @@ -175,6 +177,7 @@ void TFTDisplay::sendData(const uint8_t *data, int length) void TFTDisplay::sendColor(uint16_t color, int numPixels) { + writePixelsToFrameBuffer(color, numPixels); for (int i = 0; i < numPixels; i += DMA_BUFFER_SIZE >> 1) { int len = std::min(numPixels - i, int(DMA_BUFFER_SIZE >> 1)); @@ -186,6 +189,15 @@ void TFTDisplay::sendColor(uint16_t color, int numPixels) void TFTDisplay::setWindow(int32_t x0, int32_t y0, int32_t x1, int32_t y1) { + // store the window for future frame buffer updates + windowX0 = x0; + windowY0 = y0; + windowX1 = x1; + windowY1 = y1; + currentX = x0; + currentY = y0; + + // do the TFT window set uint8_t data[4]; #ifdef TFT_X_OFFSET x0+=TFT_X_OFFSET; diff --git a/firmware/src/TFT/TFTDisplay.h b/firmware/src/TFT/TFTDisplay.h index 29cdb2b..1b74c8a 100644 --- a/firmware/src/TFT/TFTDisplay.h +++ b/firmware/src/TFT/TFTDisplay.h @@ -1,6 +1,7 @@ #pragma once #include "driver/spi_master.h" #include "driver/gpio.h" +#include "../Serial.h" #include #define TFT_WHITE 0xFFFF @@ -37,24 +38,7 @@ #define TFT_MAD_SS 0x02 #define TFT_MAD_GS 0x01 - -#define TFT_NOP 0x00 -#define TFT_SWRST 0x01 - -#define TFT_SLPIN 0x10 -#define TFT_SLPOUT 0x11 -#define TFT_NORON 0x13 - -#define TFT_INVOFF 0x20 -#define TFT_INVON 0x21 -#define TFT_DISPOFF 0x28 -#define TFT_DISPON 0x29 -#define TFT_CASET 0x2A -#define TFT_PASET 0x2B -#define TFT_RAMWR 0x2C -#define TFT_RAMRD 0x2E -#define TFT_MADCTL 0x36 -#define TFT_COLMOD 0x3A +#define ENABLE_FRAMEBUFFER 0 #define SEND_CMD_DATA(cmd, data...) \ { \ @@ -139,6 +123,66 @@ class TFTDisplay virtual int width() { return _width; } virtual int height() { return _height; } + void saveScreenshot(uint16_t borderWidth = 20, uint16_t borderColor = TFT_BLACK) { +#if ENABLE_FRAMEBUFFER + char fname[32]; + snprintf(fname, sizeof(fname), "/fs/screenshot%03d.ppm", screenshotCount++); + FILE *file = fopen(fname, "wb"); + if (!file) { + perror("Failed to open file"); + return; + } + + Serial.printf("Saving screenshot to %s\n", fname); + + // Write the PPM header with increased dimensions for border + fprintf(file, "P6\n%zu %zu\n255\n", width() + (borderWidth * 2), + height() + (borderWidth * 2)); + + // Convert border color from RGB565 to RGB888 components + uint8_t borderR = ((borderColor >> 11) & 0x1F) * 255 / 31; + uint8_t borderG = ((borderColor >> 5) & 0x3F) * 255 / 63; + uint8_t borderB = (borderColor & 0x1F) * 255 / 31; + + // Create a buffer for one row of pixels (RGB888 format) + const size_t rowWidth = width() + (borderWidth * 2); + uint8_t *rowBuffer = new uint8_t[rowWidth * 3]; + + // Write pixel data with border + for (size_t y = 0; y < height() + (borderWidth * 2); ++y) { + uint8_t *ptr = rowBuffer; + + for (size_t x = 0; x < rowWidth; ++x) { + // If we're in the border area, write border color pixels + if (y < borderWidth || y >= height() + borderWidth || + x < borderWidth || x >= width() + borderWidth) { + *ptr++ = borderR; + *ptr++ = borderG; + *ptr++ = borderB; + } else { + // Get the RGB565 pixel from the actual image area + uint16_t rgb565 = SWAPBYTES(framebuffer[(y - borderWidth) * width() + + (x - borderWidth)]); + + // Extract and scale RGB components + *ptr++ = ((rgb565 >> 11) & 0x1F) * 255 / 31; // R + *ptr++ = ((rgb565 >> 5) & 0x3F) * 255 / 63; // G + *ptr++ = (rgb565 & 0x1F) * 255 / 31; // B + } + } + + // Write the entire row at once + fwrite(rowBuffer, 1, rowWidth * 3, file); + } + + delete[] rowBuffer; + fflush(file); + fclose(file); + Serial.println("Done Screenshot"); +#else + Serial.println("Screenshots disabled - ENABLE_FRAMEBUFFER is 0"); +#endif + } protected: int _width; int _height; @@ -172,4 +216,46 @@ class TFTDisplay // The current font Font currentFont; + + // Frame buffer rendering + int screenshotCount = 0; +#if ENABLE_FRAMEBUFFER + uint16_t framebuffer[320 * 240] = {0}; // Full display framebuffer +#endif + int32_t windowX0, windowY0, windowX1, windowY1; // Active window + int32_t currentX, currentY; // Current pixel being written + + void writePixelsToFrameBuffer(uint16_t color, size_t count) { +#if ENABLE_FRAMEBUFFER + for (size_t i = 0; i < count; ++i) { + if (currentX >= 0 && currentX < 320 && currentY >= 0 && currentY < 240) { + framebuffer[currentY * 320 + currentX] = color; // Write to framebuffer + } + + // Move to the next pixel in the window + currentX++; + if (currentX > windowX1) { + currentX = windowX0; + currentY++; + } + } +#endif + } + + void writePixelsToFrameBuffer(const uint16_t *buffer, size_t count) { +#if ENABLE_FRAMEBUFFER + for (size_t i = 0; i < count; ++i) { + if (currentX >= 0 && currentX < 320 && currentY >= 0 && currentY < 240) { + framebuffer[currentY * 320 + currentX] = buffer[i]; // Write pixel from buffer + } + + // Move to the next pixel in the window + currentX++; + if (currentX > windowX1) { + currentX = windowX0; + currentY++; + } + } +#endif + } }; \ No newline at end of file diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index 22a2124..5bf2977 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -69,8 +69,14 @@ void setup(void) vTaskDelay(100); #endif Serial.println("Starting up"); + #ifdef TFT_ST7789 + TFTDisplay *tft = new ST7789(TFT_MOSI, TFT_SCLK, TFT_CS, TFT_DC, TFT_RST, TFT_BL, TFT_WIDTH, TFT_HEIGHT); + #endif + #ifdef TFT_ILI9341 + TFTDisplay *tft = new ILI9341(TFT_MOSI, TFT_SCLK, TFT_CS, TFT_DC, TFT_RST, TFT_BL, TFT_WIDTH, TFT_HEIGHT); + #endif // navigation stack - NavigationStack *navigationStack = new NavigationStack(); + NavigationStack *navigationStack = new NavigationStack(tft); // Audio output #ifdef USE_DAC_AUDIO AudioOutput *audioOutput = new DACOutput(I2S_NUM_0); @@ -122,12 +128,6 @@ void setup(void) if (audioOutput) { audioOutput->start(15625); } - #ifdef TFT_ST7789 - TFTDisplay *tft = new ST7789(TFT_MOSI, TFT_SCLK, TFT_CS, TFT_DC, TFT_RST, TFT_BL, TFT_WIDTH, TFT_HEIGHT); - #endif - #ifdef TFT_ILI9341 - TFTDisplay *tft = new ILI9341(TFT_MOSI, TFT_SCLK, TFT_CS, TFT_DC, TFT_RST, TFT_BL, TFT_WIDTH, TFT_HEIGHT); - #endif #else Flash *fileSystem = new Flash(MOUNT_POINT); Files *files = new Files(fileSystem); @@ -149,11 +149,12 @@ void setup(void) NUNCHUK_CLOCK, NUNCHUK_DATA); #endif #ifdef SEESAW_CLOCK - AdafruitSeeSaw *seeSaw = new AdafruitSeeSaw([&](SpecKeys key, bool down) - { navigationStack->updateKey(key, down); }, - [&](SpecKeys key) - { navigationStack->pressKey(key); }); - seeSaw->begin(SEESAW_DATA, SEESAW_CLOCK); + // TODO - this seems to hang the system + // AdafruitSeeSaw *seeSaw = new AdafruitSeeSaw([&](SpecKeys key, bool down) + // { navigationStack->updateKey(key, down); }, + // [&](SpecKeys key) + // { navigationStack->pressKey(key); }); + // seeSaw->begin(SEESAW_DATA, SEESAW_CLOCK); #endif Serial.println("Running on core: " + String(xPortGetCoreID())); @@ -167,6 +168,7 @@ void setup(void) if (digitalRead(0) == LOW) { navigationStack->updateKey(SPECKEY_MENU, true); + bootButtonWasPressed = true; } else if (bootButtonWasPressed) {