File:  [Isaki's NoNo m68k/m88k emulator] / nono / vm / syncer.cpp
Revision 1.1.1.9 (vendor branch): download - view: text, annotated - select for diffs
Wed Apr 29 17:05:59 2026 UTC (2 months, 3 weeks ago) by root
Branches: MAIN, Isaki
CVS tags: v027, HEAD
nono 1.7.0

//
// nono
// Copyright (C) 2022 nono project
// Licensed under nono-license.txt
//

//
// 実時間同期
//

#include "syncer.h"
#include "config.h"
#include "event.h"
#include "monitor.h"
#include "mpu680x0.h"
#include "scheduler.h"

//#define DEBUG

#if defined(DEBUG)
#define DPRINTF(fmt...)	printf(fmt);
#else
#define DPRINTF(fmt...) /**/
#endif

// パフォーマンス測定を行う実時間間隔
static constexpr uint64 PERF_INTERVAL = 1000_msec;

// コンストラクタ
Syncer::Syncer()
	: inherited(OBJ_SYNCER)
{
	for (int i = 0; i < perfq.Capacity(); i++) {
		perfq.Enqueue(1000);
	}
}

// デストラクタ
Syncer::~Syncer()
{
}

// 初期化
bool
Syncer::Init()
{
	mpu680x0 = dynamic_cast<MPU680x0Device *>(mpu);
	thman = gMainApp.GetThreadManager();

	mode = SYNCMODE_POWEROFF;
	overslept = 0;
	dmac_idle = 1;

	// 高速モード指定
	bool fast_mode = gConfig->Find("fast-mode").AsBool();
	NotifyFullSpeed(fast_mode);

	// クロックの同期方法
	const ConfigItem& sync_item = gConfig->Find("clock-sync");
	auto clock_sync = sync_item.AsString();
	if (clock_sync == "real") {
		sync_realtime = true;
	} else if (clock_sync == "virtual") {
		sync_realtime = false;
	} else {
		sync_item.Err();
		return false;
	}

	scheduler->ConnectMessage(MessageID::SCHEDULER_SYNCMODE, this,
		ToMessageCallback(&Syncer::ChangeModeMessage));

	auto evman = GetEventManager();
	sync_event = evman->Regist(this,
		ToEventCallback(&Syncer::SyncCallback),
		"Sync");
	sync_event->time = 50_msec;

	return true;
}

void
Syncer::StartRealTime()
{
	realtime.Start();
}

void
Syncer::StopRealTime()
{
	realtime.Stop();
}

// 時間の始まり
void
Syncer::StartTime()
{
	scheduler->StartEvent(sync_event);

	// 諸々の初期化が終わった頃にタイマーを開始。
	// Stopwatch.Restart() は .Net 由来なので
	// Restart() は経過時間をリセットしてからタイマーを開始の意。
	realtime.Restart();

	rtime = nsec_to_tsec(realtime.Elapsed_nsec());
	rtime_epoch = rtime;
	vtime_epoch = scheduler->GetVirtTime();

	// パフォーマンス表示用。
	// 最初の集計が行われる起動後1秒までは、この値が表示されるので
	// インチキして 100% にしておく。
	clock_khz = mpu->GetClockSpeed();
	perf_counter = 100;
	last_perf_rtime = rtime_epoch;
	last_perf_vtime = vtime_epoch;
	next_perf_rtime = PERF_INTERVAL;
}

// CPU 状態変更(通常/STOP/HALT)を指示する。
// CPU が呼ぶはずなので自スレッドからのみ呼ばれるはず。
void
Syncer::NotifyCPUMode(uint32 cpumode)
{
	uint32 newmode;

	halt     = cpumode & SYNCMODE_CPU_HALT;
	cpu_idle = cpumode & SYNCMODE_CPU_STOP;

	newmode = (mode & ~SYNCMODE_CPU_MASK) | halt | (cpu_idle & dmac_idle);

	ChangeMode(newmode);
}

// DMAC のアクティブ状態を指示する。
void
Syncer::NotifyDMACActive(bool active)
{
	uint32 newmode;

	dmac_idle = active ? 0 : SYNCMODE_IDLE;

	newmode = (mode & ~SYNCMODE_IDLE) | (cpu_idle & dmac_idle);

	ChangeMode(newmode);
}

// 高速モード/同期モードの変更を指示する。
// true なら高速モード、false なら同期モード。
// 他スレッドから呼んでもよい。
void
Syncer::NotifyFullSpeed(bool enable)
{
	scheduler->SendMessage(MessageID::SCHEDULER_SYNCMODE, enable);
}

// キー入力状態の変更を指示する。
void
Syncer::NotifyKeyPressed(bool pressed)
{
	uint32 newmode;

	if (pressed) {
		newmode = mode | SYNCMODE_KEY;
	} else {
		newmode = mode & ~SYNCMODE_KEY;
	}

	ChangeMode(newmode);
}

// 電源状態の変更を指示する。
void
Syncer::NotifyPowerOffMode(bool poweroff)
{
	uint32 newmode;

	if (poweroff) {
		newmode = mode | SYNCMODE_POWEROFF;
	} else {
		newmode = mode & ~SYNCMODE_POWEROFF;
	}

	ChangeMode(newmode);
}

// サウンド出力状態の変更を指示する。
void
Syncer::NotifySoundRunning(bool running)
{
	uint32 newmode;

	if (running) {
		newmode = mode | SYNCMODE_SOUND;
	} else {
		newmode = mode & ~SYNCMODE_SOUND;
	}

	ChangeMode(newmode);
}

// 動作状態変更のメッセージコールバック
void
Syncer::ChangeModeMessage(MessageID msgid, uint32 arg)
{
	uint32 newmode;

	if (msgid == MessageID::SCHEDULER_SYNCMODE) {
		if (arg) {
			newmode = mode & ~SYNCMODE_SYNC;
		} else {
			newmode = mode | SYNCMODE_SYNC;
		}

		ChangeMode(newmode);
	}
}

// newmode に従ってスケジューラの動作状態を変更。
// 自スレッドから呼ぶこと。
void
Syncer::ChangeMode(uint32 newmode)
{
	// いずれかでも変化していれば..
	if (mode != newmode) {
		// 動作モード変更要因を全部調べた結果、変更が必要か。
		// mode、newmode どちらもゼロ(高速モード)か非ゼロ(通常モード)か
		// なので、等値比較ではなく二値にしてから比較。
		if ((bool)mode != (bool)newmode) {
			// モードが変わるのでここで一旦同期をとる
			DoSync();

			if (newmode == 0) {
				// 高速モードに
				putlog(2, "Fast mode");
			} else {
				// 同期モードに
				putlog(2, "Sync mode");

				// 同期用の基準点を設置。
				// ゲストが STOP 命令による定常状態にいる時は
				// 割り込みなど短期間だけ高速モードになるのを繰り返す。
				// スリープ粒度の粗い NetBSD ホストでは同期モードに
				// 切り替えるたびに基準点を移動していると、スリープ
				// しすぎてしまうので、実時間と仮想時間のズレが小さい場合は
				// 基準点を移動せず無視する。
				uint64 vtime = scheduler->GetVirtTime();
				uint64 rspan = rtime - rtime_epoch;
				uint64 vspan = vtime - vtime_epoch;
				if (vspan > rspan + overslept) {
					rtime_epoch = rtime;
					vtime_epoch = vtime;
				}
			}
		}

		// ブール比較の後で、全ビットを維持したまま代入。
		mode = newmode;
	}
	DPRINTF("ChangeMode end mode=$%x\n", mode);
}

// 同期イベント
void
Syncer::SyncCallback(Event *ev)
{
	DPRINTF("SyncCallback\n");
	DoSync();

	// 次回のイベントを再登録
	scheduler->StartEvent(ev);
}

// 同期処理
// ここで仮想時間を実時間と同期させる。
void
Syncer::DoSync()
{
	// 実時間をここで更新
	rtime = nsec_to_tsec(realtime.Elapsed_nsec());
	DPRINTF("Sync begin rtime=%s\n", SecToStr(rtime).c_str());

	// 統計情報
	sync_count++;
	// XXX どこでやる?
	if (__predict_false(rtime >= next_perf_rtime)) {
		CalcPerf();
	}

	// XXX 次回 Start する時のイベント間隔をここ(DoSync())で事前に決めている
	sync_event->time = 50_msec;

	// 同期モードなら..
	if (mode != 0) {
		uint64 vtime = scheduler->GetVirtTime();
		uint64 rspan = rtime - rtime_epoch;
		uint64 vspan = vtime - vtime_epoch;
		if (vspan > rspan) {
			// 仮想時間のほうが進んでいれば、スリープして待つ
			uint64 diff = vspan - rspan;
			uint64 prev = rtime;

			DPRINTF("Sync wait_for=%s\n", SecToStr(diff).c_str());
			scheduler->Sleep(diff);

			// スリープしすぎた時間を計測。
			// この時間にはスリープ時間を計算するための時間も含む。
			rtime = nsec_to_tsec(realtime.Elapsed_nsec());
			DPRINTF("Sync wakeup rtime=%s\n", SecToStr(rtime).c_str());
			uint64 realslept = rtime - prev;
			if (realslept > diff) {
				uint64 over = realslept - diff;
				if (over > 1_sec) {
					// スリープ中にサスペンドされたと推定する
					putlog(1, "時刻再同期(スリープ中の実時間跳躍)");
					rtime_epoch = rtime;
					vtime_epoch = vtime;
				} else if (over > overslept) {
					overslept = over;
				}
			} else if (realslept < diff) {
				uint64 under = diff - realslept;
				if (under > overslept) {
					sync_event->time = 0;
				}
			}
		} else if (rspan > vspan + overslept) {
			// 実時間のほうが時間精度より進んでいれば、サスペンドとかから
			// 復帰してきた状態だと推定して、基準時刻を再同期する。
			putlog(1, "時刻再同期(実行中の実時間跳躍)");
			rtime_epoch = rtime;
			vtime_epoch = vtime;
		} else {
			// 同期の範囲内にいるので何もしなくていい
		}
	}
}

void
Syncer::CalcPerf()
{
	uint64 vtime = scheduler->GetVirtTime();
	uint64 rperf = rtime - last_perf_rtime;
	uint64 vperf = vtime - last_perf_vtime;

	// 百分率で小数以下1桁分までとなるよう整数で計算する
	perfq.EnqueueForce((uint32)(vperf * 1000 / rperf));
	// 移動平均を求める (perfq は常に要素数分埋めてある)
	uint ma = perfq.GetMovingAverage();
	// 最下位を四捨五入して捨てる (整数%になる)
	ma = (ma + 5) / 10;
	perf_counter = ma;

	// 直近1秒の Sync 回数
	last_sync_count = sync_count;
	sync_count = 0;

	// 関係ないけど、ついでにここで MPU 内の各種移動平均をとる。
	if (mpu680x0) {
		mpu680x0->CalcStat();
	}

	uint64 mpu_exec = mpu->GetExecCount();
	last_mpu_exec_per_sec = mpu_exec - last_mpu_exec;
	last_mpu_exec = mpu_exec;

	// ついでにここで各スレッドの CPU 利用率も調べる。
	thman->CalcPerf(rtime);

	last_perf_rtime = rtime;
	last_perf_vtime = vtime;
	next_perf_rtime = rtime + PERF_INTERVAL;
}

// パフォーマンス測定用に実時間をログ出力
void
Syncer::PutlogRealtime()
{
	uint64 vtime = scheduler->GetVirtTime();

	// 電源オン時からここまでの時間を表示するだけでリセット等はしない。
	putlog(0, "Elapsed since power-on: %.3f sec, %6.2f%%",
		(double)rtime / 1_sec,
		(double)vtime / rtime * 100);

#if 0
	// イベントキューの性能評価用
	uint64 sum = 0;
	for (const auto e : events) {
		sum += e->count;
	}
	putlog(0, "%lu events done, %6.3fM/sec", sum,
		(double)sum / rtime * 1_usec);
#endif
}

// モニタ更新の下請け (Scheduler から呼ばれる)
int
Syncer::MonitorScreenSub(TextScreen& screen, uint64 tmp_vtime)
{
	int y = 0;

	// 一応ローカルコピー。
	uint32 tmp_mode = mode;
	uint64 tmp_rtime = rtime;
	uint64 sync_rt_delta = tmp_rtime - rtime_epoch;
	uint64 sync_vt_delta = tmp_vtime - vtime_epoch;

	screen.Print(0, y, "Mode: 0x%04x (", tmp_mode);
	switch (tmp_mode & SYNCMODE_CPU_MASK) {
	 case SYNCMODE_CPU_NORMAL:
		screen.Puts(14, y, TA::Off, "Run");
		break;
	 case SYNCMODE_CPU_STOP:
		screen.Puts(14, y, TA::On, "STOP");
		break;
	 case SYNCMODE_CPU_HALT:
		screen.Puts(14, y, TA::On, "HALT");
		break;
	}
	if ((tmp_mode & SYNCMODE_SYNC)) {
		screen.Puts(19, y, TA::On, "Sync");
	} else {
		screen.Puts(19, y, TA::Off, "Full");
	}
	screen.Puts(24, y, TA::OnOff((tmp_mode & SYNCMODE_KEY)), "Key");
	screen.Puts(29, y, TA::OnOff((tmp_mode & SYNCMODE_POWEROFF)), "POff");
	screen.Puts(34, y, TA::OnOff((tmp_mode & SYNCMODE_SOUND)), "Sound");
	screen.Puts(39, y, ")");
	y++;

	// 日まで表示しようとするときは桁位置を動かす
	int width = std::max(tmp_rtime, tmp_vtime) < 86400_sec ? 20 : 25;

	std::string trt = TimeToStr(tmp_rtime);
	std::string tvt = TimeToStr(tmp_vtime);
	//                     012345678901234567
	screen.Print(0, y++,  "Total  Real Time: %*s", width, trt.c_str());
	screen.Print(4, y++,      "Virtual Time: %*s", width, tvt.c_str());
	screen.Puts(0, y + 0, "Sync   Real Time:");
	screen.Puts(4, y + 1,     "Virtual Time:");
	if (tmp_mode != 0) {
		std::string srt = TimeToStr(sync_rt_delta);
		std::string svt = TimeToStr(sync_vt_delta);
		screen.Print(18, y + 0, "%*s", width, srt.c_str());
		screen.Print(18, y + 1, "%*s", width, svt.c_str());
	}
	y += 2;

	screen.Puts(0, y, "Rating MPU Speed   :");
	if (__predict_true((clock_khz % 1000) == 0)) {
		screen.Print(21, y++, "%4u.%1uMHz",
			clock_khz / 1000, (clock_khz / 100) % 10);
	} else {
		screen.Print(21, y++, "%4u.%03uMHz",
			clock_khz / 1000, clock_khz % 1000);
	}
	uint eff_khz = clock_khz * perf_counter / 100;
	screen.Print(0, y++, "Effective MPU Speed: %4u.%1uMHz (%4u%%)",
		(eff_khz / 1000), (eff_khz / 100) % 10, perf_counter);

	int x = 52;
	int y1 = 0;
	screen.Print(x, y1++, "Max OverSlept: %13s", SecToStr(overslept).c_str());
	screen.Print(x, y1++, "Sync per sec :          %4u", last_sync_count);
	std::string mpu_str = format_number(last_mpu_exec_per_sec);
	screen.Print(x, y1++, "MPU Exec/sec : %13s", mpu_str.c_str());

	return y;
}

unix.superglobalmegacorp.com

This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.