diff --git a/src/engine/engine.h b/src/engine/engine.h index 6172fcfe4..e6ffecb97 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -671,6 +671,7 @@ class DivEngine { // add every export method here friend class DivROMExport; friend class DivExportAmigaValidation; + friend class DivExportSAPR; friend class DivExportTiuna; friend class DivExportZSM; diff --git a/src/engine/export/abstract.cpp b/src/engine/export/abstract.cpp index c6d0ec799..d922e674f 100644 --- a/src/engine/export/abstract.cpp +++ b/src/engine/export/abstract.cpp @@ -47,6 +47,7 @@ void DivROMExport::logAppend(String what) { logLock.lock(); exportLog.push_back(what); logLock.unlock(); + logD("export: %s",what); } void DivROMExport::wait() { diff --git a/src/engine/export/sapr.cpp b/src/engine/export/sapr.cpp index 25defaecc..ea3bb4082 100644 --- a/src/engine/export/sapr.cpp +++ b/src/engine/export/sapr.cpp @@ -17,159 +17,140 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +// thanks asiekierka! +// I have ported your code to this ROM export framework. + #include "sapr.h" #include "../engine.h" #include "../ta-log.h" #include -#include -#include -#include +#include #include -void DivExportSAPR::run() { - int loopOrder, loopOrderRow, loopEnd; - int tick=0; - SafeWriter* w; - std::map allCmds[2]; +constexpr int MASTER_CLOCK_PREC=(sizeof(void*)==8)?8:0; +constexpr int MASTER_CLOCK_MASK=(sizeof(void*)==8)?0xff:0; - // config - String baseLabel=conf.getString("baseLabel","song"); - int firstBankSize=conf.getInt("firstBankSize",3072); - int otherBankSize=conf.getInt("otherBankSize",4096-48); - int tiaIdx=conf.getInt("sysToExport",-1); +static String ticksToTime(double rate, int ticks) { + double timing = ticks / rate; + + return fmt::sprintf("%02d:%02d.%03d", + (int) timing / 60, + (int) timing % 60, + (int) (timing * 1000) % 1000 + ); +} + +void DivExportSAPR::run() { + int sapScanlines=0; // TODO: property! + int POKEY=-1; + int IGNORED=0; + bool palTiming=(e->song.systemFlags[POKEY].getInt("clockSel",0) != 0); + int scanlinesPerFrame = (palTiming?312:262); + size_t tickCount=0; + std::vector> regs; + + if (sapScanlines <= 0) { + sapScanlines = scanlinesPerFrame; + } + //double sapRate = (palTiming?49.86:59.92) * scanlinesPerFrame / sapScanlines; + double sapRate = (palTiming?50:60) * (double)scanlinesPerFrame / (double)sapScanlines; + + // Locate system index. + for (int i=0; isong.systemLen; i++) { + if (e->song.system[i] == DIV_SYSTEM_POKEY) { + if (POKEY>=0) { + IGNORED++; + logAppendf("Ignoring duplicate POKEY id %d",i); + break; + } + POKEY=i; + logAppendf("POKEY detected as chip id %d",i); + break; + } else { + IGNORED++; + logAppendf("Ignoring chip id %d, system id %d",i,(int)e->song.system[i]); + break; + } + } + if (POKEY<0) { + logAppendf("ERROR: Could not find POKEY"); + failed=true; + running=false; + return; + } + if (IGNORED>0) { + logAppendf("WARNING: SAP export ignoring %d unsupported system%c",IGNORED,IGNORED>1?'s':' '); + } e->stop(); e->repeatPattern=false; - e->shallStop=false; e->setOrder(0); + + logAppend("playing and logging register writes..."); + e->synchronizedSoft([&]() { - // determine loop point - // bool stopped=false; - loopOrder=0; - loopOrderRow=0; - loopEnd=0; - e->walkSong(loopOrder,loopOrderRow,loopEnd); - logAppendf("loop point: %d %d",loopOrder,loopOrderRow); + double origRate = e->got.rate; + e->got.rate=sapRate; - w=new SafeWriter; - w->init(); + // Determine loop point. + int loopOrder=0; + int loopRow=0; + int loopEnd=0; + e->walkSong(loopOrder,loopRow,loopEnd); + logAppendf("loop point: %d %d",loopOrder,loopRow); + e->warnings=""; - if (tiaIdx<0 || tiaIdx>=e->song.systemLen) { - tiaIdx=-1; - for (int i=0; isong.systemLen; i++) { - if (e->song.system[i]==DIV_SYSTEM_TIA) { - tiaIdx=i; - break; - } - } - if (tiaIdx<0) { - logAppend("ERROR: selected TIA system not found"); - failed=true; - running=false; - return; - } - } else if (e->song.system[tiaIdx]!=DIV_SYSTEM_TIA) { - logAppend("ERROR: selected chip is not a TIA!"); - failed=true; - running=false; - return; - } + // Reset the playback state. + e->curOrder=0; + e->freelance=false; + e->playing=false; + e->extValuePresent=false; + e->remainingLoops=-1; - e->disCont[tiaIdx].dispatch->toggleRegisterDump(true); - - // write patterns - // bool writeLoop=false; - logAppend("recording sequence..."); - bool done=false; + // Prepare to write song data. e->playSub(false); - - // int loopTick=-1; - SAPRLast last[2]; - SAPRNew news[2]; + bool done=false; + int fracWait=0; // accumulates fractional ticks + e->disCont[POKEY].dispatch->toggleRegisterDump(true); + std::array currRegs; + while (!done) { - // TODO implement loop - // if (loopTick<0 && loopOrder==curOrder && loopOrderRow==curRow - // && (ticks-((tempoAccum+virtualTempoN)/virtualTempoD))<=0 - // ) { - // writeLoop=true; - // loopTick=tick; - // // invalidate last register state so it always force an absolute write after loop - // for (int i=0; i<2; i++) { - // last[i]=SAPRLast(); - // last[i].pitch=-1; - // last[i].ins=-1; - // last[i].vol=-1; - // } - // } - if (e->nextTick(false,true) || !e->playing) { - // stopped=!playing; + if (e->nextTick() || !e->playing) { done=true; + for (int i=0; isong.systemLen; i++) { + e->disCont[i].dispatch->getRegisterWrites().clear(); + } break; } - for (int i=0; i<2; i++) { - news[i]=SAPRNew(); - } // get register dumps - std::vector& writes=e->disCont[tiaIdx].dispatch->getRegisterWrites(); - for (const DivRegWrite& i: writes) { - switch (i.addr) { - case 0xfffe0000: - case 0xfffe0001: - news[i.addr&1].pitch=i.val; - break; - case 0xfffe0002: - news[0].sync=i.val; - break; - case 0x15: - case 0x16: - news[i.addr-0x15].ins=i.val; - break; - case 0x19: - case 0x1a: - news[i.addr-0x19].vol=i.val; - break; - default: break; + std::vector& writes=e->disCont[POKEY].dispatch->getRegisterWrites(); + if (writes.size() > 0) { + logAppendf("saprOps: found %d messages",writes.size()); + for (DivRegWrite& write: writes) + if ((write.addr & 0xF) < 9) + currRegs[write.addr & 0xF] = write.val; + writes.clear(); + } + + // write wait + tickCount++; + int totalWait=e->cycles>>MASTER_CLOCK_PREC; + fracWait+=e->cycles&MASTER_CLOCK_MASK; + totalWait+=fracWait>>MASTER_CLOCK_PREC; + fracWait&=MASTER_CLOCK_MASK; + if (totalWait>0 && !done) { + while (totalWait) { + regs.push_back(currRegs); + totalWait--; + tickCount++; } } - writes.clear(); - // collect changes - for (int i=0; i<2; i++) { - SAPRCmd cmds; - bool hasCmd=false; - if (news[i].pitch>=0 && (last[i].forcePitch || news[i].pitch!=last[i].pitch)) { - int dt=news[i].pitch-last[i].pitch; - if (!last[i].forcePitch && abs(dt)<=16) { - if (dt<0) cmds.pitchChange=15-dt; - else cmds.pitchChange=dt-1; - } - else cmds.pitchSet=news[i].pitch; - last[i].pitch=news[i].pitch; - last[i].forcePitch=false; - hasCmd=true; - } - if (news[i].ins>=0 && news[i].ins!=last[i].ins) { - cmds.ins=news[i].ins; - last[i].ins=news[i].ins; - hasCmd=true; - } - if (news[i].vol>=0 && news[i].vol!=last[i].vol) { - cmds.vol=(news[i].vol-last[i].vol)&0xf; - last[i].vol=news[i].vol; - hasCmd=true; - } - if (news[i].sync>=0) { - cmds.sync=news[i].sync; - hasCmd=true; - } - if (hasCmd) allCmds[i][tick]=cmds; - } - e->cmdStream.clear(); - tick++; - } - for (int i=0; isong.systemLen; i++) { - e->disCont[i].dispatch->getRegisterWrites().clear(); - e->disCont[i].dispatch->toggleRegisterDump(false); } + // end of song + + // done - close out. + e->got.rate=origRate; + e->disCont[POKEY].dispatch->toggleRegisterDump(false); e->remainingLoops=-1; e->playing=false; @@ -177,286 +158,30 @@ void DivExportSAPR::run() { e->extValuePresent=false; }); - if (failed) return; + logAppend("writing data..."); + progress[0].amount=0.95f; - // render commands - logAppend("rendering commands..."); - std::vector renderedCmds; - w->writeText(fmt::format( - "; Generated by Furnace " DIV_VERSION "\n" - "; Name: {}\n" - "; Author: {}\n" - "; Album: {}\n" - "; Subsong #{}: {}\n\n", - e->song.name,e->song.author,e->song.category,e->curSubSongIndex+1,e->curSubSong->name + auto w = new SafeWriter; + w->init(); + // Write SAP header: Author, name, timing, type. + w->writeText(fmt::sprintf("SAP\r\nAUTHOR \"%s\"\r\nNAME \"%s\"\r\n%s\r\nTYPE R\r\n", + e->song.author, e->song.name, palTiming ? "PAL" : "NTSC" )); - for (int i=0; i<2; i++) { - SAPRCmd lastCmd; - int lastTick=0; - int lastWait=0; - // bool looped=false; - for (auto& kv: allCmds[i]) { - // if (!looped && !stopped && loopTick>=0 && kv.first>=loopTick) { - // writeCmd(w,&lastCmd,&lastWait,loopTick-lastTick); - // w->writeText(".loop\n"); - // lastTick=loopTick; - // looped=true; - // } - writeCmd(renderedCmds,lastCmd,i,lastWait,lastTick,kv.first); - lastTick=kv.first; - lastCmd=kv.second; - } - writeCmd(renderedCmds,lastCmd,i,lastWait,lastTick,tick); - // if (stopped || loopTick<0) w->writeText(".loop\n db 0\n"); + if (sapScanlines != scanlinesPerFrame) { + // Fastplay. + w->writeText(fmt::sprintf("FASTPLAY %d\r\n", sapScanlines)); } - // compress commands - std::vector confirmedMatches; - std::vector callTicks; - int cmId=0; - int cmdSize=renderedCmds.size(); - bool* processed=new bool[cmdSize]; - memset(processed,0,cmdSize*sizeof(bool)); - logAppend("compressing! this may take a while."); - int maxCmId=(MAX(firstBankSize/1024,1))*256; - int lastMaxPMVal=100000; - logAppendf("max cmId: %d",maxCmId); - logAppendf("commands: %d",cmdSize); - while (firstBankSize>768 && cmId potentialMatches; - for (int i=0; i=cmdSize-1) break; - progress[1].amount=(float)i/(float)(cmdSize-1); - std::vector match; - int ch=renderedCmds[i].ch; - for (int j=i+1; j=cmdSize) break; - int k=0; - int ticks=0; - int size=0; - while ( - (i+k)2) match.push_back(SAPRMatch(j,j+k,size,0)); - if (k==0) k++; - j+=k; - } - if (match.empty()) { - i++; - continue; - } - // find a length that results in most bytes saved - SAPRMatches matches; - int curSize=0; - int curLength=1; - int curTicks=0; - while (true) { - int bytesSaved=-4; - bool found=false; - for (const SAPRMatch& j: match) { - if ((j.endPos-j.pos)>=curLength) { - if (!found) { - found=true; - curSize+=renderedCmds[i+curLength-1].size; - curTicks+=renderedCmds[i+curLength-1].ticks; - } - bytesSaved+=curSize-2; - } - } - if (!found) break; - if (bytesSaved>matches.bytesSaved) { - matches.length=curLength; - matches.bytesSaved=bytesSaved; - matches.ticks=curTicks; - } - curLength++; - } - if (matches.bytesSaved>0) { - matches.pos.push_back(i); - for (const SAPRMatch& j: match) { - if ((j.endPos-j.pos)>=matches.length) { - matches.pos.push_back(j.pos); - } - } - potentialMatches[i]=matches; - } - i++; - } - if (potentialMatches.empty()) { - logAppend("potentialMatches is empty"); - break; - } - int maxPMIdx=0; - int maxPMVal=0; - for (const auto& i: potentialMatches) { - if (i.second.bytesSaved>maxPMVal) { - maxPMVal=i.second.bytesSaved; - maxPMIdx=i.first; - } - } - int maxPMLen=potentialMatches[maxPMIdx].length; - for (const int i: potentialMatches[maxPMIdx].pos) { - confirmedMatches.push_back({i,i+maxPMLen,0,cmId}); - memset(processed+i,1,maxPMLen); - //std::fill(processed.begin()+i,processed.begin()+(i+maxPMLen),true); - } - callTicks.push_back(potentialMatches[maxPMIdx].ticks); - logAppendf("CM %04x added: pos=%d,len=%d,matches=%d,saved=%d",cmId,maxPMIdx,maxPMLen,potentialMatches[maxPMIdx].pos.size(),maxPMVal); - lastMaxPMVal=maxPMVal; - cmId++; + // Time. + w->writeText(fmt::sprintf("TIME %s\r\n", ticksToTime(sapRate, tickCount))); + w->writeText("\r\n"); + // Registers. + for (auto atRegs : regs) { + w->write(atRegs.data(), 9 * sizeof(uint8_t)); } + + output.push_back(DivROMExportOutput("export.sap",w)); + progress[0].amount=1.0f; - progress[1].amount=1.0f; - logAppend("generating data..."); - delete[] processed; - std::sort(confirmedMatches.begin(),confirmedMatches.end(),[](const SAPRMatch& l, const SAPRMatch& r){ - return l.pos256 that don't fill up a page - // as they tends to increase the final size due to page alignment - int cmIdLen=cmId>256?(cmId&~255):cmId; - // overlap check - for (int i=1; i<(int)confirmedMatches.size(); i++) { - if (confirmedMatches[i-1].endPos<=confirmedMatches[i].pos) continue; - logAppend("ERROR: impossible overlap found in matches list, please report"); - failed=true; - running=false; - return; - } - - // write commands - int totalSize=0; - int cnt=cmIdLen; - w->writeText(fmt::format(" .section {0}_bank0\n .align $100\n{0}_calltable",baseLabel)); - while (cnt>0) { - int cnt2=MIN(cnt,256); - w->writeText("\n .byte "); - for (int j=0; jwriteText(fmt::format("<{}_c{},",baseLabel,cmIdLen-cnt+j)); - } - for (int j=cnt2; j<256; j++) { - w->writeText("0,"); - } - w->seek(-1,SEEK_CUR); - w->writeText("\n .byte "); - for (int j=0; jwriteText(fmt::format(">{}_c{},",baseLabel,cmIdLen-cnt+j)); - } - for (int j=cnt2; j<256; j++) { - w->writeText("0,"); - } - w->seek(-1,SEEK_CUR); - w->writeText("\n .byte "); - for (int j=0; jwriteText(fmt::format("{}_c{}>>13,",baseLabel,cmIdLen-cnt+j)); - } - for (int j=cnt2; j<256; j++) { - w->writeText("0,"); - } - w->seek(-1,SEEK_CUR); - w->writeText("\n .byte "); - for (int j=0; jwriteText(fmt::format("{},",callTicks[cmIdLen-cnt+j]&0xff)); - } - w->seek(-1,SEEK_CUR); - totalSize+=768+cnt2; - cnt-=cnt2; - } - w->writeC('\n'); - if (totalSize>firstBankSize) { - logAppend("ERROR: first bank is not large enough to contain call table"); - failed=true; - running=false; - return; - } - - int curBank=0; - int bankSize=totalSize; - int maxBankSize=firstBankSize; - int curCh=-1; - std::vector callVisited=std::vector(cmIdLen,false); - auto cmIter=confirmedMatches.begin(); - for (int i=0; i<(int)renderedCmds.size(); i++) { - int writeCall=-1; - SAPRBytes cmd=renderedCmds[i]; - if (cmIter!=confirmedMatches.end() && i==cmIter->pos) { - if (cmIter->idid]) { - unsigned char idLo=cmIter->id&0xff; - unsigned char idHi=cmIter->id>>8; - cmd=SAPRBytes(cmd.ch,0,2,{idHi,idLo}); - i=cmIter->endPos-1; - } else { - writeCall=cmIter->id; - callVisited[writeCall]=true; - } - } - cmIter++; - } - if (cmd.ch!=curCh) { - if (curCh>=0) { - w->writeText(" .text x\"e0\"\n"); - totalSize++; - bankSize++; - } - if (bankSize+cmd.size>=maxBankSize) { - maxBankSize=otherBankSize; - curBank++; - w->writeText(fmt::format(" .endsection\n\n .section {}_bank{}",baseLabel,curBank)); - bankSize=0; - } - w->writeText(fmt::format("\n{}_ch{}\n",baseLabel,cmd.ch)); - curCh=cmd.ch; - } - if (bankSize+cmd.size+1>=maxBankSize) { - maxBankSize=otherBankSize; - curBank++; - w->writeText(fmt::format(" .text x\"c0\"\n .endsection\n\n .section {}_bank{}\n",baseLabel,curBank)); - totalSize++; - bankSize=0; - } - if (writeCall>=0) { - w->writeText(fmt::format("{}_c{}\n",baseLabel,writeCall)); - } - w->writeText(" .text x\""); - for (int j=0; jwriteText(fmt::format("{:02x}",cmd.buf[j])); - } - w->writeText("\"\n"); - totalSize+=cmd.size; - bankSize+=cmd.size; - } - w->writeText(" .text x\"e0\"\n .endsection\n"); - totalSize++; - logAppendf("total size: %d bytes (%d banks)",totalSize,curBank+1); - - output.push_back(DivROMExportOutput("export.asm",w)); logAppend("finished!"); @@ -464,10 +189,8 @@ void DivExportSAPR::run() { } bool DivExportSAPR::go(DivEngine* eng) { - progress[0].name="Compression"; + progress[0].name="Progress"; progress[0].amount=0.0f; - progress[1].name="Confirmed Matches"; - progress[1].amount=0.0f; e=eng; running=true; @@ -479,6 +202,7 @@ bool DivExportSAPR::go(DivEngine* eng) { void DivExportSAPR::wait() { if (exportThread!=NULL) { + logV("waiting for export thread..."); exportThread->join(); delete exportThread; } @@ -498,6 +222,6 @@ bool DivExportSAPR::hasFailed() { } DivROMExportProgress DivExportSAPR::getProgress(int index) { - if (index<0 || index>2) return progress[2]; + if (index<0 || index>1) return progress[1]; return progress[index]; } diff --git a/src/engine/export/sapr.h b/src/engine/export/sapr.h index 14a2dd8b0..817a4a855 100644 --- a/src/engine/export/sapr.h +++ b/src/engine/export/sapr.h @@ -24,7 +24,7 @@ class DivExportSAPR: public DivROMExport { DivEngine* e; std::thread* exportThread; - DivROMExportProgress progress[3]; + DivROMExportProgress progress[2]; bool running, failed, mustAbort; void run(); public: diff --git a/src/gui/about.cpp b/src/gui/about.cpp index 95818dcc3..b39ae184e 100644 --- a/src/gui/about.cpp +++ b/src/gui/about.cpp @@ -37,6 +37,7 @@ const char* aboutLine[]={ _N("A M 4 N (intro tune)"), "Adam Lederer", "akumanatt", + "asiekierka", "cam900", "djtuBIG-MaliceX", "Eknous",