apple2ix/src/timing.c

677 lines
21 KiB
C

/*
* 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);
}