From 5c9fd69ac1c06e4001f43373b4f6cfbad8d08f46 Mon Sep 17 00:00:00 2001 From: Adam Lederer Date: Sun, 4 Aug 2024 21:07:08 -0700 Subject: [PATCH] add undo to instrument editor (check for diffs on the current DivInstrument in insEdit, record them in a stack) --- src/engine/instrument.cpp | 139 ++++++++++++++++++++++++++++++++++++++ src/engine/instrument.h | 70 +++++++++++++++++-- src/fixedQueue.h | 93 ++++++++++++------------- src/gui/debugWindow.cpp | 2 +- src/gui/doAction.cpp | 4 ++ src/gui/gui.cpp | 1 + src/gui/gui.h | 5 ++ src/gui/insEdit.cpp | 48 ++++++++++++- 8 files changed, 307 insertions(+), 55 deletions(-) diff --git a/src/engine/instrument.cpp b/src/engine/instrument.cpp index ad5ebe8e1..2f80aaca1 100644 --- a/src/engine/instrument.cpp +++ b/src/engine/instrument.cpp @@ -17,6 +17,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +#include #include "dataErrors.h" #include "engine.h" #include "instrument.h" @@ -363,6 +364,144 @@ void DivInstrument::writeFeatureFM(SafeWriter* w, bool fui) { FEATURE_END; } +void MemPatch::clear() { + data = nullptr; + offset = 0; + size = 0; +} + +bool MemPatch::calcDiff(const void* pre, const void* post, size_t inputSize) { + bool diffValid = false; + size_t firstDiff = 0; + size_t lastDiff = 0; + const uint8_t* preBytes = (const uint8_t*)pre; + const uint8_t* postBytes = (const uint8_t*)post; + + for (size_t ii = 0; ii < inputSize; ++ii) { + if (preBytes[ii] != postBytes[ii]) { + lastDiff=ii; + firstDiff=diffValid ? firstDiff : ii; + diffValid=true; + } + } + + if (diffValid) { + offset = firstDiff; + size = lastDiff - firstDiff + 1; + data = new uint8_t[size]; + + // the diff is to make pre into post (MemPatch is general, not specific to + // undo), so copy from postBytes + memcpy(data, postBytes + offset, size); + } + + return diffValid; +} + +void MemPatch::applyAndReverse(void* target, size_t targetSize) { + if (size == 0) { return; } + assert(offset + size <= targetSize); + uint8_t* targetBytes = (uint8_t*)target; + + // swap this->data and its segment on target + for (size_t ii = 0; ii < size; ++ii) { + uint8_t tmp = targetBytes[offset + ii]; + targetBytes[offset + ii] = data[ii]; + data[ii] = tmp; + } +} + +void DivInstrumentUndoStep::clear() { + podPatch.clear(); + name.clear(); +} + +void DivInstrumentUndoStep::applyAndReverse(DivInstrument* target) { + if (nameValid) { + name.swap(target->name); + } + podPatch.applyAndReverse((DivInstrumentPOD*)target, sizeof(DivInstrumentPOD)); +} + +bool DivInstrumentUndoStep::makeUndoPatch(size_t processTime_, const DivInstrument* pre, const DivInstrument* post) { + processTime = processTime_; + + // create the patch that will make post into pre + podPatch.calcDiff((const DivInstrumentPOD*)post, (const DivInstrumentPOD*)pre, sizeof(DivInstrumentPOD)); + if (pre->name.compare(post->name) != 0) { + nameValid = true; + name = pre->name; + } + + return nameValid || podPatch.isValid(); +} + +void DivInstrument::recordUndoStepIfChanged(size_t processTime, const DivInstrument* old) { + DivInstrumentUndoStep step; + + // generate a patch to go back to old + if (step.makeUndoPatch(processTime, old, this)) { + + // make room + if (undoHist.size() >= undoHist.capacity()) { + DivInstrumentUndoStep* step = undoHist.front(); + delete step; + undoHist.pop_front(); + } + + // clear redo + while (!redoHist.empty()) { + delete redoHist.back(); + redoHist.pop_back(); + } + + DivInstrumentUndoStep* stepPtr = new DivInstrumentUndoStep; + *stepPtr = step; + step.clear(); // don't let it delete the data ptr that's been copied! + undoHist.push_back(stepPtr); + + logI("DivInstrument::undoHist push (%u off, %u size)", stepPtr->podPatch.offset, stepPtr->podPatch.size); + } +} + +int DivInstrument::undo() { + if (undoHist.empty()) { return 0; } + + DivInstrumentUndoStep* step = undoHist.back(); + undoHist.pop_back(); + logI("DivInstrument::undo (%u off, %u size)", step->podPatch.offset, step->podPatch.size); + step->applyAndReverse(this); + + // make room + if (redoHist.size() >= redoHist.capacity()) { + DivInstrumentUndoStep* step = redoHist.front(); + delete step; + redoHist.pop_front(); + } + redoHist.push_back(step); + + return 1; +} + +int DivInstrument::redo() { + if (redoHist.empty()) { return 0; } + + DivInstrumentUndoStep* step = redoHist.back(); + redoHist.pop_back(); + logI("DivInstrument::redo (%u off, %u size)", step->podPatch.offset, step->podPatch.size); + step->applyAndReverse(this); + + // make room + if (undoHist.size() >= undoHist.capacity()) { + DivInstrumentUndoStep* step = undoHist.front(); + delete step; + undoHist.pop_front(); + } + undoHist.push_back(step); + + return 1; +} + void DivInstrument::writeMacro(SafeWriter* w, const DivInstrumentMacro& m) { if (!m.len) return; diff --git a/src/engine/instrument.h b/src/engine/instrument.h index 49b2991d4..c4fd743cd 100644 --- a/src/engine/instrument.h +++ b/src/engine/instrument.h @@ -23,8 +23,10 @@ #include "dataErrors.h" #include "../ta-utils.h" #include "../pch.h" +#include "../fixedQueue.h" struct DivSong; +struct DivInstrument; // NOTICE! // before adding new instrument types to this struct, please ask me first. @@ -860,8 +862,7 @@ struct DivInstrumentSID2 { noiseMode(0) {} }; -struct DivInstrument { - String name; +struct DivInstrumentPOD { DivInstrumentType type; DivInstrumentFM fm; DivInstrumentSTD std; @@ -880,6 +881,63 @@ struct DivInstrument { DivInstrumentPowerNoise powernoise; DivInstrumentSID2 sid2; + DivInstrumentPOD() : + type(DIV_INS_FM) { + } +}; + +struct MemPatch { + MemPatch() : + data(nullptr) + , offset(0) + , size(0) { + } + + ~MemPatch() { + if (data) { + free(data); + } + } + + void clear(); + bool calcDiff(const void* pre, const void* post, size_t size); + void applyAndReverse(void* target, size_t inputSize); + bool isValid() const { return size > 0; } + + uint8_t* data; + size_t offset; + size_t size; +}; + +struct DivInstrumentUndoStep { + DivInstrumentUndoStep() : + name(""), + nameValid(false), + processTime(0) { + } + + MemPatch podPatch; + String name; + bool nameValid; + size_t processTime; + + void clear(); + void applyAndReverse(DivInstrument* target); + bool makeUndoPatch(size_t processTime_, const DivInstrument* pre, const DivInstrument* post); +}; + +struct DivInstrument : DivInstrumentPOD { + String name; + + /** + * undo stuff + */ + FixedQueue undoHist; + FixedQueue redoHist; + void recordUndoStepIfChanged(size_t processTime, const DivInstrument* old); + int undo(); + int redo(); + /** * these are internal functions. */ @@ -964,9 +1022,11 @@ struct DivInstrument { * @return whether it was successful. */ bool saveDMP(const char* path); - DivInstrument(): - name(""), - type(DIV_INS_FM) { + DivInstrument() : + name("") { + // clear and construct DivInstrumentPOD so it doesn't have any garbage in the padding + memset((DivInstrumentPOD*)this, 0, sizeof(DivInstrumentPOD)); + new ((DivInstrumentPOD*)this) DivInstrumentPOD; } }; #endif diff --git a/src/fixedQueue.h b/src/fixedQueue.h index 68f883edc..f781b3d33 100644 --- a/src/fixedQueue.h +++ b/src/fixedQueue.h @@ -24,7 +24,7 @@ #include "ta-log.h" template struct FixedQueue { - size_t readPos, writePos; + size_t readPos, curSize; T data[items]; T& operator[](size_t pos); @@ -41,17 +41,21 @@ template struct FixedQueue { bool push_back(const T& item); void clear(); bool empty(); + size_t writePos(); size_t size(); + size_t capacity(); FixedQueue(): readPos(0), - writePos(0) {} + curSize(0) {} }; template T& FixedQueue::operator[](size_t pos) { - if (pos>=size()) { + if (pos>=curSize) { logW("accessing invalid position. bug!"); } - return data[(readPos+pos)%items]; + size_t idx=readPos+pos; + if (idx>=items) idx-=items; + return data[idx]; } template T& FixedQueue::front() { @@ -59,12 +63,13 @@ template T& FixedQueue::front() { } template T& FixedQueue::back() { - if (writePos==0) return data[items-1]; - return data[writePos-1]; + if (curSize==0) return data[0]; + size_t idx=readPos+curSize-1; + if (idx>=items) idx-=items; + return data[idx]; } template bool FixedQueue::erase(size_t pos) { - size_t curSize=size(); if (pos>=curSize) { logW("accessing invalid position. bug!"); return false; @@ -84,72 +89,56 @@ template bool FixedQueue::erase(size_t pos) p1++; } - if (writePos>0) { - writePos--; - } else { - writePos=items-1; - } - + curSize--; return true; } template bool FixedQueue::pop() { - if (readPos==writePos) return false; - if (++readPos>=items) readPos=0; + if (curSize==0) return false; + curSize--; return true; } template bool FixedQueue::push(const T& item) { - if (writePos==(readPos-1)) { + if (curSize==items) { //logW("queue overflow!"); return false; } - if (writePos==items-1 && readPos==0) { - //logW("queue overflow!"); - return false; - } - data[writePos]=item; - if (++writePos>=items) writePos=0; + size_t idx=readPos+curSize; + if (idx>=items) { idx-=items; } + data[idx]=item; + curSize++; return true; } template bool FixedQueue::pop_front() { - if (readPos==writePos) return false; + if (curSize==0) return false; if (++readPos>=items) readPos=0; + curSize--; return true; } template bool FixedQueue::push_back(const T& item) { - if (writePos==(readPos-1)) { + if (curSize==items) { //logW("queue overflow!"); return false; } - if (writePos==items-1 && readPos==0) { - //logW("queue overflow!"); - return false; - } - data[writePos]=item; - if (++writePos>=items) writePos=0; + size_t idx=readPos+curSize; + if (idx>=items) { idx-=items; } + data[idx]=item; + curSize++; return true; } template bool FixedQueue::pop_back() { - if (readPos==writePos) return false; - if (writePos>0) { - writePos--; - } else { - writePos=items-1; - } + if (curSize==0) return false; + curSize--; return true; } template bool FixedQueue::push_front(const T& item) { - if (readPos==(writePos+1)) { - //logW("stack overflow!"); - return false; - } - if (readPos==0 && writePos==items-1) { - //logW("stack overflow!"); + if (curSize==items) { + //logW("queue overflow!"); return false; } if (readPos>0) { @@ -158,23 +147,31 @@ template bool FixedQueue::push_front(const T readPos=items-1; } data[readPos]=item; + curSize++; return true; } template void FixedQueue::clear() { readPos=0; - writePos=0; + curSize=0; } template bool FixedQueue::empty() { - return (readPos==writePos); + return curSize==0; +} + +template size_t FixedQueue::writePos() { + size_t idx=readPos+curSize; + if (idx>=items) { idx-=items; } + return idx; } template size_t FixedQueue::size() { - if (readPos>writePos) { - return items+writePos-readPos; - } - return writePos-readPos; + return curSize; +} + +template size_t FixedQueue::capacity() { + return items; } #endif diff --git a/src/gui/debugWindow.cpp b/src/gui/debugWindow.cpp index b08c60532..25969d2ec 100644 --- a/src/gui/debugWindow.cpp +++ b/src/gui/debugWindow.cpp @@ -605,7 +605,7 @@ void FurnaceGUI::drawDebug() { } if (ImGui::TreeNode("Recent Files")) { ImGui::Text("Items: %d - Max: %d",(int)recentFile.size(),settings.maxRecentFile); - ImGui::Text("readPos: %d - writePos: %d",(int)recentFile.readPos,(int)recentFile.writePos); + ImGui::Text("readPos: %d - writePos: %d",(int)recentFile.readPos,(int)recentFile.writePos()); ImGui::Indent(); for (size_t i=0; i localeExtraRanges; DivInstrument* prevInsData; + DivInstrument cachedCurIns; + DivInstrument* cachedCurInsPtr; unsigned char* pendingLayoutImport; size_t pendingLayoutImportLen; @@ -2922,6 +2924,9 @@ class FurnaceGUI { void doUndoSample(); void doRedoSample(); + void doUndoInstrument(); + void doRedoInstrument(); + void play(int row=0); void setOrder(unsigned char order, bool forced=false); void stop(); diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index 99dbf26dc..8fb588a98 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -5250,6 +5250,7 @@ void FurnaceGUI::drawInsEdit() { ImGui::SetNextWindowSizeConstraints(ImVec2(440.0f*dpiScale,400.0f*dpiScale),ImVec2(canvasW,canvasH)); } if (ImGui::Begin("Instrument Editor",&insEditOpen,globalWinFlags|(settings.allowEditDocking?0:ImGuiWindowFlags_NoDocking),_("Instrument Editor"))) { + DivInstrument* ins=nullptr; if (curIns==-2) { ImGui::SetCursorPosY(ImGui::GetCursorPosY()+(ImGui::GetContentRegionAvail().y-ImGui::GetFrameHeightWithSpacing()+ImGui::GetStyle().ItemSpacing.y)*0.5f); CENTER_TEXT(_("waiting...")); @@ -5277,6 +5278,7 @@ void FurnaceGUI::drawInsEdit() { curIns=i; wavePreviewInit=true; updateFMPreview=true; + ins = e->song.ins[curIns]; } } ImGui::EndCombo(); @@ -5299,7 +5301,7 @@ void FurnaceGUI::drawInsEdit() { ImGui::EndTable(); } } else { - DivInstrument* ins=e->song.ins[curIns]; + ins=e->song.ins[curIns]; if (updateFMPreview) { renderFMPreview(ins); updateFMPreview=false; @@ -7738,7 +7740,51 @@ void FurnaceGUI::drawInsEdit() { ImGui::EndPopup(); } + + if (ins) { + bool insChanged = ins != cachedCurInsPtr; + bool delayDiff = ImGui::IsMouseDown(ImGuiMouseButton_Left) || ImGui::IsMouseDown(ImGuiMouseButton_Right) || ImGui::GetIO().WantCaptureKeyboard; + + // check against the last cached to see if diff -- note that modifications to instruments happen outside + // drawInsEdit (e.g. cursor inputs are processed and can directly modify macro data) + if (!insChanged && !delayDiff) { + ins->recordUndoStepIfChanged(e->processTime, &cachedCurIns); + } + + if (insChanged || !delayDiff) { + cachedCurIns = *ins; + } + + cachedCurInsPtr = ins; + } else { + cachedCurInsPtr = nullptr; + } } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_INS_EDIT; ImGui::End(); } + +void FurnaceGUI::doUndoInstrument() { + if (!insEditOpen) return; + if (curIns<0 || curIns>=(int)e->song.ins.size()) return; + DivInstrument* ins=e->song.ins[curIns]; + // is locking the engine necessary? copied from doUndoSample + e->lockEngine([this,ins]() { + ins->undo(); + cachedCurInsPtr=ins; + cachedCurIns=*ins; + }); +} + +void FurnaceGUI::doRedoInstrument() { + if (!insEditOpen) return; + if (curIns<0 || curIns>=(int)e->song.ins.size()) return; + DivInstrument* ins=e->song.ins[curIns]; + // is locking the engine necessary? copied from doRedoSample + e->lockEngine([this,ins]() { + ins->redo(); + cachedCurInsPtr=ins; + cachedCurIns=*ins; + }); +}