--- nono/vm/scheduler.cpp 2026/04/29 17:05:03 1.1.1.12 +++ nono/vm/scheduler.cpp 2026/04/29 17:06:01 1.1.1.22 @@ -4,36 +4,23 @@ // Licensed under nono-license.txt // -#include "scheduler.h" -#include "config.h" -#include "mainapp.h" -#include "mfp.h" -#include "mpu680x0.h" -#include "mpu88xx0.h" -#include "mythread.h" -#include "rtc.h" -#include "uimessage.h" -#include "vm.h" +// +// スケジューラ +// // VM スケジューラは、仮想時間とイベントを管理する。 +// 仮想時間はすべて tsec で表し、StartTime() から単調増加する。 +// VM 界からは仮想時間は gScheduler->GetVirtTime() で取得する。 // -// 仮想時間は電源オン時からの nsec で表し、MPU デバイス (実際はその向こうの -// CPU コア) が管理している。VM 界からは gMPU->GetVirtTime() で取得する。 -// -// 実時間はストップウォッチ gRealtime を基準とし、こちらも nsec で表す。 -// このストップウォッチ自体は Scheduler 初期化時から常に単調増加する。 -// 電源オフ中でも RTC へのパルス供給のために単調増加し続けることに注意。 +// 実時間は Syncer が管理し、こちらも tsec で表す。 +// このストップウォッチも StartTime() から単調増加する。 +// 実時間、仮想時間とも電源オフ中でも単調増加することに注意。 // -// 「実経過時間」は、GetRealTime() (中身は gRealtime.Elapsed()) で取得でき -// o (たぶん)アプリケーション起動時から、常に増加。 +// 「実経過時間」は gRealtime.Elapsed_nsec() で取得できるが tsec で保持する。 +// o (たぶん) Scheduler スレッド開始時から、常に増加。 // o (たぶん)ホストの時刻変更やサスペンドの影響を受けず、常に増加。 -// o VM 電源オフ時でも進行する。 // o デバッガプロンプトで停止中は進行が停止する。 // という性質を持つ。 -// 「仮想経過時間」は、gMPU->GetVirtTime() で取得でき -// o 電源オン時のみ有効。 -// o (今のところ) MPU の命令境界で増加する -// という性質を持つ。 // // 同期走行モードにおいては rtime_epoch, vtime_epoch をそれぞれの基準点とし // これらと現在の実経過時間、仮想経過時間の差が同期するよう調整する。 @@ -43,110 +30,31 @@ // last_perf_rtime, last_perf_vtime として持っているので、これと現在の // 経過時間との比で求める。同期走行/高速走行の影響は受けない。 -static void *scheduler_run(void *arg); - -std::unique_ptr gScheduler; - -// パフォーマンス測定を行う実時間間隔 -static constexpr uint64 PERF_INTERVAL = 1000_msec; - -// 実時間の基準となるストップウォッチ。 -Stopwatch gRealtime; - -// 実経過時間 [nsec] を返す -static inline uint64 GetRealTime() -{ - return gRealtime.Elapsed(); -} - -#define FORMAT_FULL 0 -#define FORMAT_SEC 1 - -// 経過時間 t を文字列にして返す。 -// 文字列長は、t の大きさ(とフォーマット指定)によって以下の通り。 -// -// 0 1 2 -// 01234567890123456789012345 -// "0.mmm'uuu'nnn" 10秒未満、または FORMAT_SEC なら13桁 -// "59.mmm'uuu'nnn" 1分未満なら14桁 -// " 9:59.mmm'uuu'nnn" 1時間未満なら17桁 -// " 9:59:59.mmm'uuu'nnn" 24時間未満なら20桁 -// "999d 23:59:59.mmm'uuu'nnn" 1日以上なら25桁。これが最大幅。 -// -// FORMAT_SEC は1桁秒以下の場合 (実際には1秒未満の場合) に用いる。 -// FORMAT_FULL は 1000日経過すると桁がずれるけど、それはもういいだろう。 -// -// 10秒未満の場合だけ %2d ではなく %d で1桁切り詰めているが、これは -// FORMAT_SEC との互換性のため。その必要のない10分未満と10時間未満は -// どちらも %2d で表記し桁数を維持することに努める。 -static const std::string -TimeToStrF(uint64 t, int format) -{ - char buf[32]; - char *p; - size_t len; - int n; - - uint ns = t % 1000; - t /= 1000; - uint us = t % 1000; - t /= 1000; - uint ms = t % 1000; - t /= 1000; - - uint s, m, h, d; - if (format == FORMAT_FULL) { - s = t % 60; - t /= 60; - m = t % 60; - t /= 60; - h = t % 24; - t /= 24; - d = t; - } else { - s = t; - m = 0; - h = 0; - d = 0; - } - - p = buf; - len = sizeof(buf); - if (d) { - n = snprintf(p, len, "%3dd %02u:%02u:%02u", d, h, m, s); - p += n; - len -= n; - } else if (h) { - n = snprintf(p, len, "%2u:%02u:%02u", h, m, s); - p += n; - len -= n; - } else if (m) { - n = snprintf(p, len, "%2u:%02u", m, s); - p += n; - len -= n; - } else { - n = snprintf(p, len, "%u", s); - p += n; - len -= n; - } - n = snprintf(p, len, ".%03u'%03u'%03u", ms, us, ns); - p += n; - len -= n; - - return std::string(buf, p - buf); -} +#include "scheduler.h" +#include "event.h" +#include "monitor.h" +#include "rtc.h" +#include "syncer.h" -const std::string -TimeToStr(uint64 t) -{ - return TimeToStrF(t, FORMAT_FULL); -} +//#define CT_ON -static const std::string -SecToStr(uint64 t) -{ - return TimeToStrF(t, FORMAT_SEC); -} +#ifdef CT_ON +static uint64 ct_get1; +static uint64 ct_get2; +static uint64 ct_start; +static uint64 ct_start1; +static uint64 ct_start2; +static uint64 ct_start3; +static uint64 ct_start4; +static uint64 ct_stop1; +static uint64 ct_stop2; +static uint64 ct_eslow; +static uint64 ct_sslow; +static uint64 ct_pslow; +#define CT(name) ct_##name += 1 +#else +#define CT(name) +#endif // // スケジューラ @@ -154,736 +62,413 @@ SecToStr(uint64 t) // コンストラクタ Scheduler::Scheduler() - : inherited("Scheduler") + : inherited(OBJ_SCHEDULER) { - for (int i = 0; i < perfq.Capacity(); i++) { - perfq.Enqueue(1000); - } - - sync_event.func = (DeviceCallback_t)&Scheduler::SyncCallback; - sync_event.time = 50_msec; - sync_event.SetName("Scheduler Sync"); - - monitor.func = (MonitorCallback_t)&Scheduler::MonitorUpdate; - // サイズは Init で決まる - monitor.Regist(ID_MONITOR_SCHEDULER); + // オブジェクト名は Scheduler だがここのスレッド名は VM くらいのほうが + // 通りがよさそうだ。 + SetThreadName("VM"); + + slow_top = -1; + slow_top_vtime = UINT64_MAX; + + monitor = gMonitorManager->Regist(ID_MONITOR_SCHEDULER, this); + monitor->SetCallback(&Scheduler::MonitorScreen); + // サイズは最終的に Init2() で決まる。 + monitor->SetSize(80, 9); } // デストラクタ Scheduler::~Scheduler() { - Terminate(); + TerminateThread(); } // 初期化 bool Scheduler::Init() { - // この時点でイベントの個数は確定しているはず(してなければならない) - monitor.SetSize(72, 10 + gEvents.size()); - - // 起動時引数指定の高速モード - SetFullSpeed(gMainApp.fast_mode); - - // スケジューラ(VM)スレッド起動 - pthread_create(&thread, NULL, scheduler_run, NULL); - thread_created = true; + evman = GetEventManager(); + syncer = GetSyncer(); return true; } -// スレッドエントリ関数 -void * -scheduler_run(void *arg) +// 初期化(VM から呼ばれる) +bool +Scheduler::Init2() { - PTHREAD_SETNAME("VM"); + // イベントの個数が確定した(してなければならない) + monitor->AddHeight(evman->all.size()); - gScheduler->ThreadRun(); - return NULL; + return true; } -// スレッドエントリ関数(の実体) +// スレッドエントリ関数 void Scheduler::ThreadRun() { - // ここが rtime の基準点。 - // Stopwatch.Restart() は .Net 由来なので - // Restart() は経過時間をリセットしてからタイマーを開始の意。 - gRealtime.Restart(); - rtime = 0; - next_rtc_rtime = 31.25_msec; - - for (;;) { - if (RunOff() == false) - return; - if (RunOn() == false) - return; - } -} - -// 電源オフ時のスケジューラメインループ。 -// false ならこのスレッドの終了指示。 -bool -Scheduler::RunOff() -{ - uint32 oldreq; + SetThreadAffinityHint(AffinityClass::Heavy); - ispower = false; + // 電源オンオフに関わらず、ここが rtime, vtime の基準点 + StartTime(); - oldreq = -1; for (;;) { - // RunOn() と違ってこっちは処理したフラグだけを下ろす。 - - // デバッグ表示も前回から変化した時だけ。 - uint32 req = atomic_reqflag.load(); - if (__predict_false(req != oldreq)) { - if (loglevel >= 2) { - PutlogReqFlag(req); + // 条件変数の正しい使い方としては request の参照にも mtx ロックが必要 + // だが、ここでロック取らずに参照したことによって request が立った + // ことを見落としても、ループ一回回ってもう一度ここに来るだけなので + // たぶん致命的なことはないはず。 + if (__predict_false(request)) { + uint32 req; + { + std::lock_guard lock(mtx); + req = request; + request = 0; + } + if ((req & REQUEST_EXIT)) { + break; } - oldreq = req; - } - - // 終了 - if ((atomic_reqflag & REQ_EXIT)) { - return false; - } - - if ((atomic_reqflag & REQ_RESTART)) { - // 電源オフ中に RESTART は通常来るはずはないが、来てしまったら - // メニューから RESTART 操作中に VM が電源オフしてしまったのかも - // 知れないので、単に電源を入れる。 - atomic_reqflag &= ~REQ_RESTART; - atomic_reqflag |= REQ_POWER_ON; - } - // 電源オン指示 - if ((atomic_reqflag & REQ_POWER_ON)) { - // 電源オン要求の場合 - - // 全デバイス電源オン - for (const auto& d : gDevices) { - if (!d->PowerOn()) { - return false; + for (; req; req &= req - 1) { + uint id = __builtin_ctz(req); + if (id < MessageID::MAX_REQUEST) { + InvokeMessage(id, 0); + } else { + DispatchMessage(); } } + } - // 続いて全デバイスをリセット - for (const auto& d : gDevices) { - d->ResetHard(); - } - - // POWER_ON は処理したので下ろす。他のフラグは触らない。 - atomic_reqflag &= ~REQ_POWER_ON; - // 電源オンは RESET も兼ねる (RESET は RunOn() 内で行う) - atomic_reqflag |= REQ_RESET; - - return true; + // 先頭のイベントを取得して停止 + Event *ev; + if (__predict_true(fast != NULL)) { + CT(get1); + ev = fast; + fast = NULL; + } else { + CT(get2); + ev = slow[slow_top--]; + slow_top_vtime = slow[slow_top]->vtime; } + ev->active = false; + + // 時刻更新 + vtime = ev->vtime; - // RTC はホスト時間で 32Hz (31.25msec) ごとにカウントアップするが、 - // usleep() はそこまでの精度がないことと、RunRTC() は前回カウント - // アップ時刻からの差で処理しており入力が多少ぶれても構わないので、 - // 適当にその周期の半分強で sleep してみる。 - usleep(16_msec / 1_usec); - - // RTC をホスト時間で進める。 - // (電源オンするまでは gRealTime は進み続けている) - rtime = GetRealTime(); - RunRTC(); + // コールバック + ev->count++; + auto dev = ev->GetOwner(); + auto func = ev->GetCallback(); + (dev->*(func))(ev); } - __unreachable(); } -// 電源オン時のスケジューラメインループ。 -// false ならこのスレッドの終了指示。 -bool -Scheduler::RunOn() +// 時間の始まり +void +Scheduler::StartTime() { - uint64 last_event_vtime; // 直近のイベント発生時刻 (統計用) - Statistics event_stat; - uint64 last_sync_rtime; - - ispower = true; - - // パフォーマンス計測は電源オン時のみなので、ここで初期化 - last_perf_rtime = rtime; - last_perf_vtime = gMPU->GetVirtTime(); - next_perf_rtime = rtime + PERF_INTERVAL; - // 最初の集計が行われる起動後1秒までは、この値が表示されるので - // インチキして 100% にしておく。 - perf_counter = 100; - - last_event_vtime = last_perf_vtime; - - // 実時間系の初期化 - rtime = GetRealTime(); - last_sync_rtime = 0; - overslept = 0; - sync_event.Start(); - - for (;;) { - vtime = gMPU->GetVirtTime(); - - uint32 req = atomic_reqflag.exchange(0); - if (__predict_false(req != 0)) { - - // デバッグ表示 - if (loglevel >= 2) { - PutlogReqFlag(req); - } - - uint32 newmode = mode; - - // 動作モード変更要因を一つずつ調べる - if ((req & REQ_CPU_MODE)) { - // CPU 状態が new_cpumode に変更になった - newmode &= ~SCHED_CPU_MASK; - newmode |= new_cpumode; - req &= ~REQ_CPU_MODE; - } - if ((req & REQ_USER_MODE)) { - // ユーザからの高速/同期モード指示が変更になった - newmode &= ~SCHED_SYNC; - newmode |= new_usermode; - req &= ~REQ_USER_MODE; - } - if ((req & REQ_BOOT_MODE)) { - // ブートページが変更になった - newmode &= ~SCHED_BOOT; - newmode |= new_bootmode; - req &= ~REQ_BOOT_MODE; - } - if ((req & REQ_KEY_MODE)) { - // キー入力中かどうか - newmode &= ~SCHED_KEY; - newmode |= new_keymode; - req &= ~REQ_KEY_MODE; - } - - // いずれかでも変化していれば.. - if (mode != newmode) { - // 動作モード変更要因を全部調べた結果、変更が必要か。 - // mode、newmode どちらもゼロ(高速モード)か非ゼロ(通常モード)か - // なので、等値比較ではなく二値にしてから比較。 - if ((bool)mode != (bool)newmode) { - // モードが変わるのでここで一旦同期をとる - auto over = Sync(); - if (overslept < over) { - overslept = over; - } - - if (newmode == 0) { - // 高速モードに - putlog(1, "高速モード"); - - // 同期モードを抜けた時刻 - last_sync_rtime = rtime; - - // 高速モードでは RAM は直接アクセス - ::direct_ram_size = gRAM->GetSize(); - - } else { - // 同期モードに - putlog(1, "同期モード"); - - // 同期モードでは RAM アクセスはデバイス経由 - ::direct_ram_size = 0; - - // 同期用の基準点を設置。 - // ゲストが STOP 命令による定常状態にいる時は - // 割り込みなど短期間だけ高速モードになるのを繰り返す。 - // スリープ粒度の粗い NetBSD ホストでは同期モードに - // 切り替えるたびに基準点を移動していると、スリープ - // しすぎてしまうので、短期間だけ高速モードを出入り - // した分は基準点を移動せず無視する。 - if (rtime - last_sync_rtime > overslept) { - rtime_epoch = rtime; - vtime_epoch = vtime; - } - } - } - - // ブール比較の後で、全ビットを維持したまま代入。 - mode = newmode; - } - - if ((req & REQ_RESTART)) { - // 電源オン中に RESTART が来たら電源オンを予約して電源オフ。 - // UIMessage::POWEROFF は GUI にアプリケーションを終了させる - // ためのものなのでここでは呼ばない。 - atomic_reqflag |= REQ_POWER_ON; - - // 全デバイス電源オフ - for (const auto& d : gDevices) { - if (!d->PowerOff()) { - return false; - } - } - - return true; - } - - if ((req & REQ_POWER_OFF)) { - // 電源オフ要求の場合 - - // 全デバイス電源オフ - for (const auto& d : gDevices) { - if (!d->PowerOff()) { - return false; - } - } - - // GUI に通知 - UIMessage::Post(UIMessage::POWEROFF); - - return true; - } - - if ((req & REQ_RESET)) { - // リセット要求の場合 - - // 全デバイスをリセット - for (const auto& d : gDevices) { - d->ResetHard(); - } - - req &= ~REQ_RESET; - } - - if ((req & REQ_EXIT)) { - // 終了要求 - return false; - } - - // これいるかな? - assertmsg(req == 0, "req=$%x", req); - } - - // パフォーマンス表示 - if (__predict_false(rtime >= next_perf_rtime)) { - uint64 rperf = rtime - last_perf_rtime; - uint64 vperf = vtime - last_perf_vtime; - - // 百分率で小数以下1桁分までとなるよう整数で計算する - perfq.EnqueueForce((uint32)(vperf * 1000 / rperf)); - // 移動平均を求める (perfq は常に要素数分埋めてある) - int ma = 0; - int qlen = perfq.Length(); - for (int i = 0; i < qlen; i++) { - auto v = perfq.Peek(i); - ma += v; - } - ma /= qlen; - // 最下位を四捨五入して捨てる (整数%になる) - ma = (ma + 5) / 10; - perf_counter = ma; - - // 直近1秒の Sync 回数 - last_sync_count = sync_count; - sync_count = 0; - - // 直近1秒のイベント統計 - last_event_stat = event_stat; - event_stat.Clear(); - - last_perf_rtime = rtime; - last_perf_vtime = vtime; - next_perf_rtime = rtime + PERF_INTERVAL; - } - - // イベントリストを調べ現時点から最も早く発生するイベントまでの必要 - // 時間を求めて、この時間分だけ MPU を駆動する。 - // MPU 実行中にデバイスアクセスなどによってスケジューラにイベントが - // 登録されると、その命令の終了とともに処理を打ち切って戻ってくる。 - - // delta_vtime が今回進める仮想時間。 - // とりあえず初期値を仮置きしとくけど、イベントは通常 20msec 以内には - // 必ず起きるので (例えば LUNA の sysclk は 10 or 16 msec 周期)、 - // この初期値は実質上限値。20msec 程度より大きければいくらでもよくて - // 100msec とかでいいんだけど、二進数で置数しやすいよう - // 0x200'0000 (約33.5msec) にしておく。 - uint64 delta_vtime = 0x200'0000; - - // イベント - for (;;) { - Event *e; - // ロック区間 - { - std::lock_guard lock(evcs); - - if (eventq.empty()) { - break; - } - e = eventq.top(); - if (e->vtime > vtime) { - // まだ時刻に到達していない - delta_vtime = e->vtime - vtime; - break; - } - - // 到達したのでこのイベントをリストから削除 - e->active = false; - eventq.pop(); - } - - // イベント発生間隔 (統計情報) - // t は前回と今回のイベント予定時刻の間隔。 - uint64 t = e->vtime - last_event_vtime; - event_stat.Add(t); - last_event_vtime = e->vtime; - - // コールバック - e->dev->putlogf(3, lstr("イベント '%s' 時刻到達", - e->GetName().c_str())); - ((e->dev)->*(e->func))(*e); - - // 同時刻のイベントがあるかも知れないのでなくなるまで調べる - } - - // RTC をホスト時間で進める - // (同期イベントによって sleep したかもしれないので、その後がよかろう) - RunRTC(); - - // CPU を駆動 - new_cpumode = gMPU->Run(delta_vtime); - - // CPU 側の走行状態が変わったら、このループに通知 - if (__predict_false(((mode ^ new_cpumode) & SCHED_CPU_MASK))) { - atomic_reqflag |= REQ_CPU_MODE; + // イベントキューを空にする + for (auto *ev : evman->all) { + if (ev->active) { + StopEvent(ev); } + ev->count = 0; } - __unreachable(); + + // 時間をリセットする。 + // 呼び出しの依存関係に注意。 + vtime = 0; // required by Sync + syncer->StartTime(); // required by RTC + GetRTCDevice()->StartTime(); } -// 同期イベント +// スレッド終了指示 +// (当然他スレッドから呼ぶことになる) void -Scheduler::SyncCallback(Event& ev) +Scheduler::Terminate() { - Sync(); - - // 次回のイベントを再登録 - ev.Start(); + std::lock_guard lock(mtx); + request |= REQUEST_EXIT; + cv.notify_one(); } -// 同期処理 -// ここで仮想時間を実時間と同期させる。 -// スリープしすぎた実時間を返す。 -uint64 -Scheduler::Sync() +// 指定のイベントを開始する。 +// すでに同イベントが登録されている場合は古いイベントを削除してから +// 新しいイベントを再登録となる。 +// イベントはワンショットのみ。 +void +Scheduler::RestartEvent(Event *ev) { - uint64 over = 0; - - // 実時間をここで更新 - rtime = GetRealTime(); - - // 統計情報 - sync_count++; - - // 同期モードなら.. - if (mode != 0) { - uint64 rspan = rtime - rtime_epoch; - uint64 vspan = vtime - vtime_epoch; - if (vspan > rspan) { - // 仮想時間のほうが進んでいれば、スリープして待つ - uint64 diff = vspan - rspan; - struct timespec ts; - ts.tv_sec = diff / (1000 * 1000 * 1000); - ts.tv_nsec = diff % (1000 * 1000 * 1000); - nanosleep(&ts, NULL); - - // スリープしすぎた時間を計測。 - // この時間にはスリープ時間を計算するための時間も含む。 - uint64 prev = rtime; - rtime = GetRealTime(); - uint64 realslept = rtime - prev; - if (realslept > diff) { - over = realslept - diff; - } - } else { - // 実時間のほうが進んでいれば、間に合ってない - - // XXX ここで間引き運転とか - } + if (ev->IsRunning()) { + StopEvent(ev); } - return over; + + StartEvent(ev); } -// RTC は常にホスト時刻で動いており -// 32Hz = 31.25msec ごとにパルスを入れる。 -// VM の電源がオフでも実行する。 +// 指定のイベントを開始する。 +// このイベントが登録されてないことが確定できる場合のみこちらが使える。 +// イベントはワンショットのみ。 void -Scheduler::RunRTC() +Scheduler::StartEvent(Event *ev) { - while (rtime >= next_rtc_rtime) { - gRTC->ClockIn(); - next_rtc_rtime += 31.25_msec; + CT(start); +#ifdef CT_ON + if (ct_start % 100'000'000 == 0) { + printf("get*=%" PRIu64 " [%" PRIu64 ", %" PRIu64 "]\n", + (ct_get1 + ct_get2), ct_get1, ct_get2); + printf("start*=%" PRIu64 + " [%" PRIu64 ", %" PRIu64 ", %" PRIu64 ", %" PRIu64 "]\n", + ct_start, ct_start1, ct_start2, ct_start3, ct_start4); + printf("stop*=%" PRIu64 " [%" PRIu64 ", %" PRIu64 "]\n", + (ct_stop1 + ct_stop2), ct_stop1, ct_stop2); + printf("eslow=%" PRIu64 " sslow=%" PRIu64 " pslow=%" PRIu64 "\n", + ct_eslow, ct_sslow, ct_pslow); + printf("slow_top=%d vtime=%" PRIu64 "\n", slow_top, slow_top_vtime); + } +#endif + + // この assert を有効にすると 10 ポイント性能が低下する(502%->492%) + //assertmsg(ev->IsRunning() == false, "%s", ev->GetName().c_str()); + + // vtime は仮想絶対時刻 [tsec]、time は仮想相対時間 [tsec] + ev->vtime = vtime + ev->time; + + ev->active = true; + + if (__predict_true(ev->vtime <= slow_top_vtime)) { + if (__predict_true(fast == NULL)) { + CT(start1); + fast = ev; + } else { + if (__predict_false(ev->vtime <= fast->vtime)) { + CT(start2); + PushSlow(fast); + fast = ev; + } else { + CT(start3); + PushSlow(ev); + } + } + } else { + CT(start4); + EnqueueSlow(ev); } } -// 必要ならスレッドの終了を指示して完了まで待つ。 -void -Scheduler::Terminate() -{ - if (thread_created) { - RequestExit(); - Wait(); +// 実時間間隔を指定してイベントを開始する。 +// rt_now はイベント発行者の実時間での現在時刻で、 +// rt_period は次回のイベントまでの実時間間隔。 +void +Scheduler::StartRealtimeEvent(Event *ev, uint64 rt_now, uint64 rt_period) +{ + // 実時間をもとに仮想時間軸上の推定を行う。 + + uint64 rtime = syncer->GetRealTime(); + + if (rtime > rt_now + rt_period) { + // 実時間がさらに 1 period 以上進んでいる。 + // 実時間に追いつかないといけないので、イベントの + // 周期を短くして回復運転させる。 + // 周期を短くしすぎるとゲストが処理できなくなるので、 + // 10% カットに留める。 + ev->time = rt_period - rt_period / 10; + } else if (rtime < rt_now - rt_period) { + // イベントがさらに 1 period 以上進んでいる。 + // 実時間を追い越してしまっているので、イベントの + // 周期を長くして待つ。長くする方はいくらでも長くしていいので、 + // 連続した場合はどんどん長くしていく。 + ev->time += rt_period; + } else { + // おおむね同期している + ev->time = rt_period; } + + RestartEvent(ev); } -// スレッドの完了まで待つ。 +// ev は slow_top には来ない前提 void -Scheduler::Wait() +Scheduler::EnqueueSlow(Event *ev) { - if (thread_created) { - pthread_join(thread, NULL); - thread_created = false; + CT(eslow); + + int s = slow_top; + slow[++slow_top] = slow[s]; + + for (; --s >= 0; ) { + if (ev->vtime <= slow[s]->vtime) { + slow[s + 1] = ev; + return; + } else { + slow[s + 1] = slow[s]; + } } + slow[0] = ev; } -// スレッド終了を指示する。 void -Scheduler::RequestExit() +Scheduler::PushSlow(Event *ev) { - atomic_reqflag |= REQ_EXIT; + CT(pslow); + + slow[++slow_top] = ev; + slow_top_vtime = ev->vtime; } -// 動作モードを設定する。 -// true なら高速モード、false なら同期モード。 +// 指定のイベントを停止する。 +// 指定されたイベントが登録されていなければ何もしない。 void -Scheduler::SetFullSpeed(bool enable) +Scheduler::StopEvent(Event *ev) { - if (enable) { - new_usermode = 0; + ev->active = false; + + if (__predict_false(ev == fast)) { + CT(stop1); + fast = NULL; } else { - new_usermode = SCHED_SYNC; + CT(stop2); + StopSlowEvent(ev); } - atomic_reqflag |= REQ_USER_MODE; } -// ブートページモードを設定する。 -// 戻り値は現在の動作モードがブートモード(ROM)なら true。 -// (VM::SwitchBootPage から呼ぶので個別には呼ばないこと) void -Scheduler::RequestBootPageMode(bool isrom) +Scheduler::StopSlowEvent(Event *ev) { - if (isrom) { - new_bootmode = SCHED_BOOT; - } else { - new_bootmode = 0; + CT(sslow); + + int s = slow_top; + + for (; s >= 0; s--) { + if (ev == slow[s]) { + for (; s < slow_top; s++) { + slow[s] = slow[s + 1]; + } + slow_top--; + if (slow_top < 0) { + slow_top_vtime = UINT64_MAX; + } else { + slow_top_vtime = slow[slow_top]->vtime; + } + break; + } } - atomic_reqflag |= REQ_BOOT_MODE; } -// キー入力中かどうかを設定する。 -// true ならキー入力中、false なら入力中でない。 +// メッセージハンドラを登録する。 void -Scheduler::RequestKeyPressed(bool pressed) +Scheduler::ConnectMessage(MessageID msgid, Device *dev, MessageCallback_t func) { - if (pressed) { - new_keymode = SCHED_KEY; - } else { - new_keymode = 0; - } - atomic_reqflag |= REQ_KEY_MODE; + assert(msgid < MessageID::MAX); + + auto& mh = message_handlers[msgid]; + assertmsg(mh.dev == NULL, + "%s: msgid=%d connected again", __func__, (int)msgid); + mh.dev = dev; + mh.func = func; } -// REQ_* フラグのデバッグ表示 +// メッセージをディスパッチして、登録されていればハンドラを呼び出す。 void -Scheduler::PutlogReqFlag(uint32 req) const +Scheduler::DispatchMessage() { - static const char * const cpumode_str[] = { - "Normal", - "STOP", - "HALT", - "HALTXXX", - }; - char buf[128]; - int len = 0; + uint64 msg; - buf[0] = '\0'; + while (msgq.Dequeue(&msg)) { + MessageID msgid = (MessageID)(msg & 0xff); + uint32 arg = (msg >> 32); -#define SET(FL, MSG) \ - if ((req & (FL))) len += snprintf(buf + len, sizeof(buf) - len, ",%s", MSG) - - SET(REQ_EXIT, "EXIT"); - SET(REQ_RESTART, "RESTART"); - SET(REQ_RESET, "RESET"); - SET(REQ_POWER_OFF, "POWER_OFF"); - SET(REQ_POWER_ON, "POWER_ON"); - if ((req & REQ_USER_MODE)) { - len += snprintf(buf + len, sizeof(buf) - len, ",USER:%s", - (new_usermode == 0) ? "Fast" : "Sync"); - } - if ((req & REQ_CPU_MODE)) { - len += snprintf(buf + len, sizeof(buf) - len, ",CPU:%s", - cpumode_str[new_cpumode & 3]); - } - if ((req & REQ_BOOT_MODE)) { - len += snprintf(buf + len, sizeof(buf) - len, ",BOOT:%s", - (new_bootmode == 0) ? "Normal" : "Boot"); - } - if ((req & REQ_KEY_MODE)) { - len += snprintf(buf + len, sizeof(buf) - len, ",KEY:%s", - (new_keymode == 0) ? "-" : "Pressed"); + // MessageID::MPU_TRACE_ALL だけ特別対応。 + // MPU_TRACE メッセージの宛先は(最大)2つあるが、 + // スレッドを越える時は1メッセージにしておきたいので、 + // MPU_TRACE_ALL が来たらここで分岐。 + if (msgid == MessageID::MPU_TRACE_ALL) { + InvokeMessage(MessageID::MPU_TRACE_MAIN, arg); + InvokeMessage(MessageID::MPU_TRACE_XP, arg); + } else { + assert(msgid < MessageID::MAX); + InvokeMessage(msgid, arg); + } } +} - if (buf[0] != '\0') { - buf[0] = '<'; - strlcat(buf, ">", sizeof(buf)); +// メッセージハンドラを呼び出す。登録されてなければ何もしない。 +void +Scheduler::InvokeMessage(MessageID msgid, uint32 arg) +{ + auto h = message_handlers[msgid]; + if (h.dev != NULL) { + (h.dev->*(h.func))(msgid, arg); } - putlogn("%sreq=$%04x%s", (ispower ? "" : "Off "), req, buf); } -// 指定のイベントを開始する。 -// すでに同イベントが登録されている場合は古いイベントを削除してから -// 新しいイベントを再登録となる。 -// イベントはワンショットのみ。 +// メッセージを送る。 +// VM スレッド以外から呼び出しても良い。 void -Scheduler::StartEvent(Event *newev) +Scheduler::SendMessage(MessageID msgid, uint32 arg) { - bool updated = false; - - // time は仮想相対時間 [nsec]、vtime は仮想絶対時刻 [nsec] - newev->vtime = gMPU->GetVirtTime() + newev->time; - - evcs.lock(); - // すでにあれば削除して... - if (newev->active) { - updated = eventq.Erase(newev); - } - // 自身を追加 - eventq.push(newev); - newev->active = true; + std::lock_guard lock(mtx); - // 先頭なら再スケジュールのため CPU 実行をここで打ち切り - if (eventq.top() == newev) { - gMPU->Release(); + if (msgid < MessageID::MAX_REQUEST) { + request |= 1U << (uint)msgid; + } else { + uint64 msg = (uint64)msgid | (((uint64)arg) << 32); + if (msgq.Enqueue(msg) == false) { + putlog(0, "Message queue exhausted !! msgid=%d", (int)msgid); + } + request |= REQUEST_MESSAGE; } - evcs.unlock(); - - newev->dev->putlogf(3, lstr("イベント '%s' %s %d.%03d usec 後", - newev->GetName().c_str(), - updated ? "更新" : "開始", - (int)(newev->time / 1000), - (int)(newev->time % 1000))); + cv.notify_one(); } -// 指定のイベントを停止する。 -// 指定されたイベントが登録されていなければ何もしない。 +// 指定時間が経過するか、リクエストが起きるまでスリープ void -Scheduler::StopEvent(Event *event) +Scheduler::Sleep(uint64 time) { - bool found = false; - - evcs.lock(); - found = eventq.Erase(event); - evcs.unlock(); - event->active = false; - - // イベントを停止した場合は MPU の実行中断はしなくてよい。 - // 1msec 後にイベントを追加した後、やっぱりそのイベントを取り消した - // 場合 (LUNA の電源オフとか) はここで MPU 処理を中断するよりも - // そのまま 1msec 走って問題ない。 - - if (found) { - event->dev->putlogf(3, lstr("イベント '%s' 停止", - event->GetName().c_str())); - } + std::unique_lock lock(mtx); + cv.wait_for(lock, std::chrono::nanoseconds(time), [&] { + return (request != 0); + }); + // リクエストフラグが立ってもここでは何もしない } void -Scheduler::MonitorUpdate(Monitor *, TextScreen& screen) +Scheduler::MonitorScreen(Monitor *, TextScreen& screen) { - int x, y; + int y; screen.Clear(); - x = 0; - y = 0; - int clock_khz = gMPU->GetClockSpeed(); + y = syncer->MonitorScreenSub(screen, vtime); - screen.Print(0, y, "Mode: 0x%04x (", mode); - if ((mode & SCHED_SYNC)) { - screen.Puts(14, y, TA::On, "Sync"); - } else { - screen.Puts(14, y, TA::Off, "Full"); - } - switch (mode & SCHED_CPU_MASK) { - case SCHED_CPU_NORMAL: - screen.Puts(19, y, TA::Off, "Run"); - break; - case SCHED_CPU_STOP: - screen.Puts(19, y, TA::On, "STOP"); - break; - case SCHED_CPU_HALT: - screen.Puts(19, y, TA::On, "HALT"); - break; - } - screen.Puts(24, y, TA::OnOff((mode & SCHED_BOOT)), "Boot"); - screen.Puts(29, y, TA::OnOff((mode & SCHED_KEY)), "Key"); - screen.Puts(33, y, ")"); + // 0 1 2 3 + // 012345678901234567890123456789 + // Event Time Remain Time + // 3.123'456'789 3.123'456'789 + // + // 3 4 5 6 7 + // 01234567890123456789012345678901234567890123456789 + // Description Count + // 012345678901234567890123456789 999,999,999,999,999 + // 18,446,744,073,709,551,615 y++; - screen.Print(0, y++, "Req: %08x", (uint32)atomic_reqflag); - screen.Print(0, y++, "MPU Speed: %d.%03dMHz", - (clock_khz / 1000), (clock_khz % 1000)); - uint64 rt = rtime; - uint64 vt = vtime; - - auto s_rt = TimeToStr(rt); - auto s_vt = TimeToStr(vt); - std::string s_sync_rt {}; - std::string s_sync_vt {}; - if (mode != 0) { - uint64 sync_rt = rtime - rtime_epoch; - uint64 sync_vt = vtime - vtime_epoch; - s_sync_rt = TimeToStr(sync_rt); - s_sync_vt = TimeToStr(sync_vt); - } - // 日 まで表示しようとするときは桁位置を動かす - int width = std::max(rt, vt) < 86400_sec ? 20 : 25; - - screen.Print(0, y++, "Total Real Time: %*s", width, s_rt.c_str()); - screen.Print(4, y++, "Virtual Time: %*s", width, s_vt.c_str()); - screen.Print(0, y++, "Sync Real Time: %*s", width, s_sync_rt.c_str()); - screen.Print(4, y++, "Virtual Time: %*s", width, s_sync_vt.c_str()); - - screen.Print(0, y++, "Performance : %4d%%", perf_counter); - - x = 44; - y = 0; - screen.Print(x, y++, "Max OverSlept: %13s", SecToStr(overslept).c_str()); - screen.Print(x, y++, "Sync per sec : %4u", last_sync_count); - screen.Print(x, y++, "Event per sec: %8u", - (uint)last_event_stat.Count()); - screen.Print(x, y++, "Evt Intvl Min: %13s", - SecToStr(last_event_stat.Min()).c_str()); - screen.Print(x, y++, "Evt Intvl Avg: %13s", - SecToStr(last_event_stat.Average()).c_str()); - screen.Print(x, y++, "Evt Intvl Max: %13s", - SecToStr(last_event_stat.Max()).c_str()); - - // 0 1 2 3 4 5 6 - // 01234567890123456789012345678901234567890123456789012345678901234568 - // Set Time Remain Time Description - // 3.123'456'789 3.123'456'789 012345678901234567890123456789 - x = 0; - y = 9; - screen.Puts(x + 0, y, "Event Time"); - screen.Puts(x + 15, y, "Remain Time"); - screen.Puts(x + 30, y, "Description"); + screen.Puts(0, y, "Event Time"); + screen.Puts(15, y, "Remain Time"); + screen.Puts(30, y, "Description"); + screen.Puts(75, y, "Count"); y++; - for (const auto& e : gEvents) { + for (auto *ev : evman->all) { uint64 rem; TA attr; - if (e->active) { + + if (ev->IsRunning()) { attr = TA::Normal; - if (e->vtime > vtime) { - rem = e->vtime - vtime; + if (ev->vtime > vtime) { + rem = ev->vtime - vtime; } else { // XXX 実際には起きないけど、この表示をするにあたって // スケジューラスレッドと一切調停していないので、仮想時刻が - // e->cycle (イベント発生時刻) をすぎていることがある。 + // ev->vtime (イベント発生時刻) をすぎていることがある。 // すぎてるので残り時間 0 と表示しておく。 rem = 0; } @@ -891,10 +476,21 @@ Scheduler::MonitorUpdate(Monitor *, Text attr = TA::Disable; rem = 0; } - screen.Print(x, y++, attr, - "%13s %13s %-30s", - SecToStr(e->time).c_str(), + + if (ev->count != ev->last_count) { + attr = TA::Normal; + ev->last_count = ev->count; + } + + // 先にイベント回数を表示。 + // 21文字以上になったら上位桁が長い Description で消されても + // 大勢に影響はないだろう。 + std::string countstr = format_number(ev->count); + screen.Print(54, y, attr, "%26s", countstr.c_str()); + screen.Print(0, y, attr, "%14s %14s %s ", + SecToStr(ev->time).c_str(), SecToStr(rem).c_str(), - e->GetName().c_str()); + ev->GetName().c_str()); + y++; } }