static enum TimerType getIrqTimerMode(uint8_t ciaIdx) { if (envIsPSID() == 1) { return vicIsIrqActive() ? RASTER_TIMER : CIA1_TIMER; } else { // todo: the below impl neglects that both raster and cia1 interrupts may be active at the same // time. the strategy to give priority to raster (and then ignore the cia1) works for some of // the affected songs (see Face_It.sid) but it is not correct if (vicIsIrqActive()) { return RASTER_TIMER; } else if (isCiaActive(ciaIdx)) { return CIA1_TIMER; } else { return NO_IRQ; } } }
enum timertype getIrqTimerMode(struct timer *t) { if (sIsPSID == 1) { return isRasterIrqActive() ? RASTER_TIMER : CIA1_TIMER; } else { // todo: the below impl neglects that both raster and cia1 interrupts may be active at the same // time. the strategy to give priority to raster (and then ignore the cia1) works for some of // the affected songs (see Face_It.sid) but it is not correct if (isRasterIrqActive()) { return RASTER_TIMER; } else if (isCiaActive(t)) { return CIA1_TIMER; } else { return NO_IRQ; } } }
static void runScreenSimulation(int16_t *synthBuffer, uint32_t cyclesPerScreen, uint16_t samplesPerCall, int16_t **synthTraceBufs) { setupVolumeHack(); // cpu cycles used during processing int32_t irqCycles= 0, nmiCycles= 0, mainProgCycles= 0; // only mainProgCycles can become negative.. // reminder: timings of IRQ and NMI are calculated as if these where independent (which is // of course flawed). A proper premptive scheduling is not simulated. int32_t availableIrqCycles= cyclesPerScreen; // cpu cycles still available for processing (limitation: only the waiting time is currently considered..) int32_t availableNmiCycles= cyclesPerScreen; uint32_t synthPos= 0; // start time for the samples synthesized from SID settings (measured in cycles) /* * process NMI and IRQ interrupts in their order of appearance - and as long as they fit into this screen */ uint32_t currentIrqTimer= forwardToNextInterrupt(getIrqTimerMode(CIA1), CIA1, availableIrqCycles); // FIXME: the assumption that only RSID uses NMI meanwhile proved to be wrong (see MicroProse_Soccer_V1.sid // tracks >4) .. also an IRQ run may also update the NMI schedule! flawed: ciaForwardToNextInterrupt // calculation does NOT consider the time when the timer is actually started but only the time that some // interrupt returns (which will lead to distortions) uint32_t currentNmiTimer= envIsRSID() ? ciaForwardToNextInterrupt(CIA2, availableNmiCycles) : NO_INT; // KNOWN LIMITATION: ideally all 4 timers should be kept in sync - not just A/B within the same unit! // however progs that actually rely on multiple timers (like Vicious_SID_2-Carmina_Burana.sid) probably // need timer info that is continuously updated - so that effort would only make sense in a full fledged // cycle-by-cycle emulator. uint8_t hack= vicIsIrqActive() && isCiaActive(CIA1); uint32_t tNmi= currentNmiTimer; // NMI simulation progress in cycles uint32_t tIrq= currentIrqTimer; // IRQ simulation progress in cycles uint32_t tDone= 0; uint16_t mainProgStep= 100; // periodically give the main prog a chance to run.. uint16_t mainProgStart= mainProgStep; if (0 || (envIsRSID() && (currentIrqTimer == NO_INT) && (currentNmiTimer == NO_INT))) { _mainLoopOnlyMode= 1; mainProgStart= 0; // this might be the better choice in any case.. but to avoid regression testing lets use it here for now.. } else { _mainLoopOnlyMode= 0; // Wonderland_XII-Digi_part_4 has high interrupt rate.. #define ABORT_FUSE 400 // better safe than sorry.. no need to crash the player with an endless loop uint16_t fuse; for (fuse= 0; (fuse<ABORT_FUSE) && ((currentIrqTimer != NO_INT) || (currentNmiTimer != NO_INT)) ; fuse++) { // periodically give unused cycles to main program (needed, e.g. by THCM's stuff) if (tDone > mainProgStart) { // todo: the handing of main prog cycles is rather flawed (timing for main code that would normally run before // 'mainProgStep'). In an alternative "improved" impl I ran "main" directly before "processInterrupt" calls below - letting // "main" use all cycles unused up to that moment. also I had added interruption for "LDA#00; BEQ .." based endless main-loop // logic (which would require IRQ logic to break out) to avoid unnecessary cycle usage.. while those changes work fine for // THCM's recent stuff they broke stuff like "Arkanoid", etc (so I rolled them back for now..) // -> now that the D012 handling has been improved I might give it another try.. int32_t availableMainCycles= tDone - (nmiCycles + irqCycles + mainProgCycles); if (availableMainCycles > 0) { processMain(cyclesPerScreen, mainProgStart, availableMainCycles); mainProgCycles+= availableMainCycles; } mainProgStart= tDone + mainProgStep; } // FIXME: better allow NMI to interrupt IRQ, i.e. set respective cycle-limit // accordingly - and then resume the IRQ later. see Ferrari_Formula_One.sid if (!isIrqNext(currentIrqTimer, currentNmiTimer, tIrq, tNmi)) { // handle next NMI tDone= tNmi; // problem: songs like Arcade_Classics re-set the d418 volume nowhere else but in their // NMI digi-player: when only used for the short intervals between NMIs the respective changing // settings do not seem to pose any problems for the rendering of the regular SID output // (or are even intentionally creating some kind of tremolo) but when SID output rendering // is performed for longer intervals (e.g. like is done here) then "wrong" settings are also // used for these longer intervals which may cause audible clicking/pauses in the output. // if the handled NMIs were preemtively scheduled/properly timed, then renderSynth() could // be performed also for these shorter intervals.. but unfortunately they are not and respective // rendering cannot be done here, i.e. the IRQ based rendering then has no clue regarding the // validity/duration of respectice NMI-volume infos.. nmiCycles+= processInterrupt(NMI_OFFSET_MASK, getNmiVector(), tNmi, cyclesPerScreen); availableNmiCycles-= currentNmiTimer; if (availableNmiCycles <= 0) { availableNmiCycles= 0; // see unsigned use below } currentNmiTimer= ciaForwardToNextInterrupt(CIA2, availableNmiCycles); tNmi+= currentNmiTimer; // will be garbage on last run.. } else { // handle next IRQ tDone= tIrq; renderSynth(synthBuffer, cyclesPerScreen, samplesPerCall, synthPos, tIrq, synthTraceBufs); synthPos= tIrq; if (hack) { // in case CIA1 has also been configured to trigger IRQs, our current "raster IRQ or timer IRQ" impl // is obviously flawed.. this hack will fix simple scenarios like Cycles.sid ciaSignalUnderflow(CIA1, TIMER_A); } // FIXME: multi-frame IRQ handling would be necessary to deal with songs like: Musik_Run_Stop.sid uint32_t usedCycles= processInterrupt(IRQ_OFFSET_MASK, getIrqVector(), tIrq, _irqTimeout); // flaw: IRQ might have switched off NMI, e.g. Vicious_SID_2-Blood_Money.sid if (usedCycles >=_irqTimeout) { // IRQ gets aborted due to a timeout if (cpuIrqFlag()) { // this IRQ handler does not want to be interrupted.. (either the code got stuck due to some impossible // condition - e.g. waiting for a timer which we don't update - or it is an endless IRQ routine that needs to // continue..) } else { // this IRQ handler was intentionally waiting to get interrupted - and it would normally not have burned // the same amount of cycles (we have no chance to get the timing right here..) usedCycles= 1; } } irqCycles+= usedCycles; availableIrqCycles-= currentIrqTimer; if (availableIrqCycles <= 0) { availableIrqCycles= 0; // see unsigned use below } currentIrqTimer= forwardToNextInterrupt(getIrqTimerMode(CIA1), CIA1, availableIrqCycles); tIrq+= currentIrqTimer; // will be garbage on last run.. if (envIsTimerDrivenPSID() && (currentIrqTimer != NO_INT)) { if(usedCycles > currentIrqTimer) { currentIrqTimer= NO_INT; // hack for Eye_of_the_Tiger } } // some dumbshit timer-PSIDs do not ackn IRQ (MasterComposer rips & Automatas.sid) if(envIsTimerDrivenPSID()) { ciaReadMem(0xdc0d); } } } #ifdef DEBUG if (fuse >= ABORT_FUSE) { EM_ASM_({ console.log('fuse blown: ' + $0); }, fuse); EM_ASM_({ console.log('last IRQ timer: ' + $0); }, currentIrqTimer);