/* * Apple // emulator for *ix * * This software package is subject to the GNU General Public License * version 3 or later (your choice) as published by the Free Software * Foundation. * * Copyright 2013-2015 Aaron Culliney * */ #include "common.h" #define DEBUG_TIMING 0 // enable to print timing stats #if DEBUG_TIMING # define TIMING_LOG(...) LOG(__VA_ARGS__) #else # define TIMING_LOG(...) #endif #define DISK_MOTOR_QUIET_NSECS (NANOSECONDS_PER_SECOND>2) // cycle counting double cycles_persec_target = CLK_6502; unsigned long cycles_count_total = 0; // Running at spec ~1MHz, this will approach overflow in ~4000secs (for 32bit architectures) unsigned int cycles_video_frame = 0; int cycles_speaker_feedback = 0; static int32_t cycles_checkpoint_count = 0; // scaling and speed adjustments static bool auto_adjust_speed = true; static bool is_paused = false; static unsigned long _pause_spinLock = SPINLOCK_INIT; double cpu_scale_factor = 1.0; double cpu_altscale_factor = 1.0; bool is_fullspeed = false; bool alt_speed_enabled = false; // misc static bool emul_reinitialize_audio = false; static bool emul_pause_audio = false; static bool emul_resume_audio = false; static bool emul_video_dirty = false; static bool cpu_shutting_down = false; pthread_t cpu_thread_id = 0; pthread_mutex_t interface_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t dbg_thread_cond = PTHREAD_COND_INITIALIZER; pthread_cond_t cpu_thread_cond = PTHREAD_COND_INITIALIZER; // ----------------------------------------------------------------------------- struct timespec timespec_diff(struct timespec start, struct timespec end, bool *negative) { struct timespec t; if (negative) { *negative = false; } // if start > end, swizzle... if ( (start.tv_sec > end.tv_sec) || ((start.tv_sec == end.tv_sec) && (start.tv_nsec > end.tv_nsec)) ) { t=start; start=end; end=t; if (negative) { *negative = true; } } // assuming time_t is signed ... if (end.tv_nsec < start.tv_nsec) { t.tv_sec = end.tv_sec - start.tv_sec - 1; t.tv_nsec = 1000000000 + end.tv_nsec - start.tv_nsec; } else { t.tv_sec = end.tv_sec - start.tv_sec; t.tv_nsec = end.tv_nsec - start.tv_nsec; } return t; } struct timespec timespec_add(struct timespec start, unsigned long nsecs) { start.tv_nsec += nsecs; if (start.tv_nsec > NANOSECONDS_PER_SECOND) { start.tv_sec += (start.tv_nsec / NANOSECONDS_PER_SECOND); start.tv_nsec %= NANOSECONDS_PER_SECOND; } return start; } static void _timing_initialize(double scale) { is_fullspeed = (scale > CPU_SCALE_FASTEST_PIVOT); if (!is_fullspeed) { cycles_persec_target = CLK_6502 * scale; } speaker_reset(); //TIMING_LOG("ClockRate:%0.2lf ClockCyclesPerSpeakerSample:%0.2lf", cycles_persec_target, speaker_cyclesPerSample()); } #if !TESTING static #endif void reinitialize(void) { #if !TESTING ASSERT_ON_CPU_THREAD(); #endif cycles_count_total = 0; cycles_video_frame = 0; #if !TEST_CPU video_scannerReset(); #endif #if TESTING extern unsigned long (*testing_getCyclesCount)(void); if (testing_getCyclesCount) { cycles_count_total = testing_getCyclesCount(); } #endif vm_initialize(); cpu65_init(); timing_initialize(); MB_Reset(); } void timing_initialize(void) { #if !TESTING assert(cpu_isPaused() || (pthread_self() == cpu_thread_id)); #endif _timing_initialize(alt_speed_enabled ? cpu_altscale_factor : cpu_scale_factor); } void timing_toggleCPUSpeed(void) { assert(cpu_isPaused() || (pthread_self() == cpu_thread_id)); alt_speed_enabled = !alt_speed_enabled; timing_initialize(); } static void timing_reinitializeAudio(void) { ASSERT_NOT_ON_CPU_THREAD(); SPIN_LOCK_FULL(&_pause_spinLock); #if !TESTING assert(cpu_isPaused()); #endif emul_reinitialize_audio = true; emul_pause_audio = false; emul_resume_audio = false; emul_video_dirty = false; SPIN_UNLOCK_FULL(&_pause_spinLock); } void cpu_pause(void) { ASSERT_NOT_ON_CPU_THREAD(); SPIN_LOCK_FULL(&_pause_spinLock); do { if (is_paused) { break; } // CPU thread will be paused when it next tries to acquire interface_mutex LOG("PAUSING CPU..."); if (!emul_reinitialize_audio) { emul_pause_audio = true; } pthread_mutex_lock(&interface_mutex); is_paused = true; } while (0); SPIN_UNLOCK_FULL(&_pause_spinLock); } void cpu_resume(void) { ASSERT_NOT_ON_CPU_THREAD(); SPIN_LOCK_FULL(&_pause_spinLock); do { if (!is_paused) { break; } // CPU thread will be unblocked to acquire interface_mutex if (!emul_reinitialize_audio) { emul_resume_audio = true; emul_video_dirty = true; } LOG("RESUMING CPU..."); is_paused = false; pthread_mutex_unlock(&interface_mutex); } while (0); SPIN_UNLOCK_FULL(&_pause_spinLock); } bool cpu_isPaused(void) { return is_paused; } #if TESTING void timing_setVideoDirty(void) { emul_video_dirty = true; } #endif bool timing_shouldAutoAdjustSpeed(void) { double speed = alt_speed_enabled ? cpu_altscale_factor : cpu_scale_factor; return auto_adjust_speed && (speed <= CPU_SCALE_FASTEST_PIVOT); } static void *cpu_thread(void *dummyptr) { #ifndef NDEBUG // Spamsung Galaxy Y running Gingerbread triggers this, wTf?! ASSERT_ON_CPU_THREAD(); #endif LOG("cpu_thread : initialized..."); struct timespec deltat = { 0 }; struct timespec disk_motor_time = { 0 }; struct timespec t0 = { 0 }; // the target timer struct timespec ti = { 0 }; // actual before time sample struct timespec tj = { 0 }; // actual after time sample bool negative = false; long drift_adj_nsecs = 0; // generic drift adjustment between target and actual int debugging_cycles = 0; unsigned long dbg_ticks = 0; #if DEBUG_TIMING int speaker_neg_feedback = 0; int speaker_pos_feedback = 0; unsigned long dbg_cycles_executed = 0; #endif audio_init(); speaker_init(); MB_Initialize(); run_args.emul_reinitialize = 1; cpu_runloop: do { LOG("CPUTHREAD %lu LOCKING FOR MAYBE INITIALIZING AUDIO ...", (unsigned long)cpu_thread_id); pthread_mutex_lock(&interface_mutex); if (emul_reinitialize_audio) { emul_reinitialize_audio = false; speaker_destroy(); extern void MB_SoftDestroy(void); MB_SoftDestroy(); audio_shutdown(); audio_init(); speaker_init(); extern void MB_SoftInitialize(void); MB_SoftInitialize(); } pthread_mutex_unlock(&interface_mutex); LOG("UNLOCKING FOR MAYBE INITIALIZING AUDIO ..."); if (run_args.emul_reinitialize) { reinitialize(); } LOG("cpu_thread : begin main loop ..."); clock_gettime(CLOCK_MONOTONIC, &t0); do { ////SCOPE_TRACE_CPU("CPU mainloop"); // -LOCK----------------------------------------------------------------------------------------- SAMPLE ti if (UNLIKELY(emul_pause_audio)) { emul_pause_audio = false; audio_pause(); } pthread_mutex_lock(&interface_mutex); if (UNLIKELY(emul_resume_audio)) { emul_resume_audio = false; audio_resume(); } if (UNLIKELY(emul_video_dirty)) { emul_video_dirty = false; video_setDirty(A2_DIRTY_FLAG); } clock_gettime(CLOCK_MONOTONIC, &ti); deltat = timespec_diff(t0, ti, &negative); if (UNLIKELY(deltat.tv_sec)) { if (!is_fullspeed) { TIMING_LOG("NOTE : serious divergence from target time ..."); } t0 = ti; deltat = (struct timespec){ 0 }; } t0 = timespec_add(t0, EXECUTION_PERIOD_NSECS); // expected interval drift_adj_nsecs = negative ? ~deltat.tv_nsec : deltat.tv_nsec; // Determine the count of 65c02 cycles to execute { static int speaker_wedged_count = 0; run_args.cpu65_cycles_to_execute = (cycles_persec_target / 1000); // cycles_persec_target * EXECUTION_PERIOD_NSECS / NANOSECONDS_PER_SECOND if (!is_fullspeed) { // Speaker backend (real-time soundcard) actually drives us! run_args.cpu65_cycles_to_execute += cycles_speaker_feedback; } if (UNLIKELY(run_args.cpu65_cycles_to_execute <= 0)) { run_args.cpu65_cycles_to_execute = 0; if (++speaker_wedged_count >= SOUNDCORE_ERROR_MAX<<1) { speaker_wedged_count = 0; emul_reinitialize_audio = true; } } else { speaker_wedged_count = 0; } } MB_StartOfCpuExecute(); if (UNLIKELY(is_debugging)) { debugging_cycles = run_args.cpu65_cycles_to_execute; } do { if (UNLIKELY(is_debugging)) { run_args.cpu65_cycles_to_execute = 1; } run_args.cpu65_cycle_count = 0; cycles_checkpoint_count = 0; cpu65_run(&run_args); // run emulation for cpu65_cycles_to_execute cycles ... #if DEBUG_TIMING dbg_cycles_executed += run_args.cpu65_cycle_count; #endif if (UNLIKELY(is_debugging)) { debugging_cycles -= run_args.cpu65_cycle_count; timing_checkpointCycles(); if (debugger_shouldBreak() || (debugging_cycles <= 0)) { int err = 0; if ((err = pthread_cond_signal(&dbg_thread_cond))) { LOG("pthread_cond_signal : %d", err); } if ((err = pthread_cond_wait(&cpu_thread_cond, &interface_mutex))) { LOG("pthread_cond_wait : %d", err); } if (debugging_cycles <= 0) { break; } } if (run_args.emul_reinitialize) { pthread_mutex_unlock(&interface_mutex); goto cpu_runloop; } } } while (UNLIKELY(is_debugging)); MB_UpdateCycles(); // TODO : modularize MB and other peripheral card cycles/interrupts ... speaker_flush(); // play audio TRACE_CPU_BEGIN("advance scanner"); video_scannerUpdate(); TRACE_CPU_END(); clock_gettime(CLOCK_MONOTONIC, &tj); pthread_mutex_unlock(&interface_mutex); // -UNLOCK--------------------------------------------------------------------------------------- SAMPLE tj if (timing_shouldAutoAdjustSpeed() && !is_fullspeed) { disk_motor_time = timespec_diff(disk6.motor_time, tj, &negative); if (UNLIKELY(negative)) { LOG("WHOA... time went backwards #1! Did you just cross a timezone?"); disk_motor_time.tv_sec = 1; } if (!speaker_isActive() && !video_isDirty(A2_DIRTY_FLAG) && (disk6.disk[disk6.drive].file_name != NULL) && !disk6.motor_off && (disk_motor_time.tv_sec || disk_motor_time.tv_nsec > DISK_MOTOR_QUIET_NSECS) ) { TIMING_LOG("auto switching to full speed"); _timing_initialize(CPU_SCALE_FASTEST); } } if (!is_fullspeed) { deltat = timespec_diff(ti, tj, &negative); if (UNLIKELY(negative)) { LOG("WHOA... time went backwards #2! Did you just cross a timezone?"); deltat.tv_sec = 1; } long sleepfor = 0; if (LIKELY(!deltat.tv_sec)) { sleepfor = EXECUTION_PERIOD_NSECS - drift_adj_nsecs - deltat.tv_nsec; } if (sleepfor <= 0) { // lagging ... static time_t throttle_warning = 0; if (t0.tv_sec - throttle_warning > 0) { TIMING_LOG("not sleeping to catch up ... %ld . %ld", deltat.tv_sec, deltat.tv_nsec); throttle_warning = t0.tv_sec; } } else { deltat.tv_sec = 0; deltat.tv_nsec = sleepfor; ////TRACE_CPU_BEGIN("sleep"); nanosleep(&deltat, NULL); ////TRACE_CPU_END(); } #if DEBUG_TIMING // collect timing statistics if (speaker_neg_feedback > cycles_speaker_feedback) { speaker_neg_feedback = cycles_speaker_feedback; } if (speaker_pos_feedback < cycles_speaker_feedback) { speaker_pos_feedback = cycles_speaker_feedback; } if ((dbg_ticks % NANOSECONDS_PER_SECOND) == 0) { TIMING_LOG("tick:(%ld.%ld) real:(%ld.%ld) cycles exe: %d ... speaker feedback: %d/%d", t0.tv_sec, t0.tv_nsec, ti.tv_sec, ti.tv_nsec, dbg_cycles_executed, speaker_neg_feedback, speaker_pos_feedback); dbg_cycles_executed = 0; speaker_neg_feedback = 0; speaker_pos_feedback = 0; } #endif if ((dbg_ticks % NANOSECONDS_PER_SECOND) == 0) { dbg_ticks = 0; } } if (timing_shouldAutoAdjustSpeed() && is_fullspeed) { disk_motor_time = timespec_diff(disk6.motor_time, tj, &negative); if (UNLIKELY(negative)) { LOG("WHOA... time went backwards #3! Did you just cross a timezone?"); disk_motor_time.tv_sec = 1; } if (speaker_isActive() || video_isDirty(A2_DIRTY_FLAG) || (disk6.motor_off && (disk_motor_time.tv_sec || disk_motor_time.tv_nsec > DISK_MOTOR_QUIET_NSECS)) ) { double speed = alt_speed_enabled ? cpu_altscale_factor : cpu_scale_factor; if (speed <= CPU_SCALE_FASTEST_PIVOT) { TIMING_LOG("auto switching to configured speed"); _timing_initialize(speed); } } } if (UNLIKELY(run_args.emul_reinitialize)) { break; } if (UNLIKELY(emul_reinitialize_audio)) { break; } if (UNLIKELY(cpu_shutting_down)) { break; } } while (1); if (UNLIKELY(cpu_shutting_down)) { break; } } while (1); speaker_destroy(); MB_Destroy(); audio_shutdown(); cpu_thread_id = 0; cpu_pause(); disk6_eject(0); disk6_eject(1); return NULL; } bool timing_isCPUThread(void) { return pthread_self() == cpu_thread_id; } void timing_startCPU(void) { cpu_shutting_down = false; assert(cpu_thread_id == 0); int err = TEMP_FAILURE_RETRY(pthread_create(&cpu_thread_id, NULL, (void *)&cpu_thread, (void *)NULL)); assert(!err); } void timing_stopCPU(void) { cpu_shutting_down = true; LOG("Emulator waiting for CPU thread clean up..."); if (pthread_join(cpu_thread_id, NULL)) { LOG("OOPS: pthread_join of CPU thread ..."); } } // Called when accurate global cycle count info is needed void timing_checkpointCycles(void) { ASSERT_ON_CPU_THREAD(); const int32_t d = run_args.cpu65_cycle_count - cycles_checkpoint_count; assert(d >= 0); cycles_video_frame += d; #if !TESTING cycles_count_total += d; #else unsigned long previous_cycles_count_total = cycles_count_total; cycles_count_total += d; if (UNLIKELY(cycles_count_total < previous_cycles_count_total)) { extern void (*testing_cyclesOverflow)(void); if (testing_cyclesOverflow) { testing_cyclesOverflow(); } } #endif cycles_checkpoint_count = run_args.cpu65_cycle_count; } // ---------------------------------------------------------------------------- bool timing_saveState(StateHelper_s *helper) { bool saved = false; int fd = helper->fd; do { uint32_t lVal = 0; uint8_t serialized[4] = { 0 }; assert(cpu_scale_factor >= 0); assert(cpu_altscale_factor >= 0); lVal = (cpu_scale_factor * 100.); serialized[0] = (uint8_t)((lVal & 0xFF000000) >> 24); serialized[1] = (uint8_t)((lVal & 0xFF0000 ) >> 16); serialized[2] = (uint8_t)((lVal & 0xFF00 ) >> 8); serialized[3] = (uint8_t)((lVal & 0xFF ) >> 0); if (!helper->save(fd, serialized, sizeof(serialized))) { break; } lVal = (cpu_altscale_factor * 100.); serialized[0] = (uint8_t)((lVal & 0xFF000000) >> 24); serialized[1] = (uint8_t)((lVal & 0xFF0000 ) >> 16); serialized[2] = (uint8_t)((lVal & 0xFF00 ) >> 8); serialized[3] = (uint8_t)((lVal & 0xFF ) >> 0); if (!helper->save(fd, serialized, sizeof(serialized))) { break; } uint8_t bVal = alt_speed_enabled ? 1 : 0; if (!helper->save(fd, &bVal, sizeof(bVal))) { break; } saved = true; } while (0); return saved; } bool timing_loadState(StateHelper_s *helper) { bool loaded = false; int fd = helper->fd; do { uint32_t lVal = 0; uint8_t serialized[4] = { 0 }; if (!helper->load(fd, serialized, sizeof(uint32_t))) { break; } lVal = (uint32_t)(serialized[0] << 24); lVal |= (uint32_t)(serialized[1] << 16); lVal |= (uint32_t)(serialized[2] << 8); lVal |= (uint32_t)(serialized[3] << 0); cpu_scale_factor = lVal / 100.; if (!helper->load(fd, serialized, sizeof(uint32_t))) { break; } lVal = (uint32_t)(serialized[0] << 24); lVal |= (uint32_t)(serialized[1] << 16); lVal |= (uint32_t)(serialized[2] << 8); lVal |= (uint32_t)(serialized[3] << 0); cpu_altscale_factor = lVal / 100.; uint8_t bVal = 0; if (!helper->load(fd, &bVal, sizeof(bVal))) { break; } alt_speed_enabled = !!bVal; timing_initialize(); loaded = true; } while (0); return loaded; } // ---------------------------------------------------------------------------- static void timing_prefsChanged(const char *domain) { (void)domain; float fVal = 1.0; cpu_scale_factor = prefs_parseFloatValue(PREF_DOMAIN_VM, PREF_CPU_SCALE, &fVal) ? fVal / 100.f : 1.f; if (cpu_scale_factor < CPU_SCALE_SLOWEST) { cpu_scale_factor = CPU_SCALE_SLOWEST; } if (cpu_scale_factor > CPU_SCALE_FASTEST_PIVOT) { cpu_scale_factor = CPU_SCALE_FASTEST; } cpu_altscale_factor = prefs_parseFloatValue(PREF_DOMAIN_VM, PREF_CPU_SCALE_ALT, &fVal) ? fVal / 100.f : 1.f; if (cpu_altscale_factor < CPU_SCALE_SLOWEST) { cpu_altscale_factor = CPU_SCALE_SLOWEST; } if (cpu_altscale_factor > CPU_SCALE_FASTEST_PIVOT) { cpu_altscale_factor = CPU_SCALE_FASTEST; } static float audioLatency = 0.f; float latency = prefs_parseFloatValue(PREF_DOMAIN_AUDIO, PREF_AUDIO_LATENCY, &fVal) ? fVal : 0.25f; #define SMALL_EPSILON (1.f/1024.f) if (fabsf(audioLatency - latency) > SMALL_EPSILON) { audioLatency = latency; audio_setLatency(latency); timing_reinitializeAudio(); } static bool mbEnabled = false; bool bVal = false; bool enabled = prefs_parseBoolValue(PREF_DOMAIN_AUDIO, PREF_MOCKINGBOARD_ENABLED, &bVal) ? bVal : true; if (enabled != mbEnabled) { mbEnabled = enabled; MB_SetEnabled(enabled); timing_reinitializeAudio(); } auto_adjust_speed = prefs_parseBoolValue(PREF_DOMAIN_INTERFACE, PREF_DISK_FAST_LOADING, &bVal) ? bVal : true; } static __attribute__((constructor)) void _init_timing(void) { prefs_registerListener(PREF_DOMAIN_VM, &timing_prefsChanged); prefs_registerListener(PREF_DOMAIN_AUDIO, &timing_prefsChanged); prefs_registerListener(PREF_DOMAIN_INTERFACE, &timing_prefsChanged); }