--- nono/vm/scheduler.cpp 2026/04/29 17:04:39 1.1.1.5 +++ nono/vm/scheduler.cpp 2026/04/29 17:05:03 1.1.1.12 @@ -12,26 +12,161 @@ #include "mpu88xx0.h" #include "mythread.h" #include "rtc.h" +#include "uimessage.h" #include "vm.h" // VM スケジューラは、仮想時間とイベントを管理する。 // -// 仮想時間は MPU の起動時からのクロックサイクルの経過数で表すこととする。 -// このエミュレータはシングルプロセッサシステムのみがターゲットであり、 -// 実行中に動作クロックが変わることはないので、これで問題ない。 -// VM 界からは gMPU->total_cycle() で取得する。 +// 仮想時間は電源オン時からの nsec で表し、MPU デバイス (実際はその向こうの +// CPU コア) が管理している。VM 界からは gMPU->GetVirtTime() で取得する。 +// +// 実時間はストップウォッチ gRealtime を基準とし、こちらも nsec で表す。 +// このストップウォッチ自体は Scheduler 初期化時から常に単調増加する。 +// 電源オフ中でも RTC へのパルス供給のために単調増加し続けることに注意。 +// +// 「実経過時間」は、GetRealTime() (中身は gRealtime.Elapsed()) で取得でき +// o (たぶん)アプリケーション起動時から、常に増加。 +// o (たぶん)ホストの時刻変更やサスペンドの影響を受けず、常に増加。 +// o VM 電源オフ時でも進行する。 +// o デバッガプロンプトで停止中は進行が停止する。 +// という性質を持つ。 +// 「仮想経過時間」は、gMPU->GetVirtTime() で取得でき +// o 電源オン時のみ有効。 +// o (今のところ) MPU の命令境界で増加する +// という性質を持つ。 +// +// 同期走行モードにおいては rtime_epoch, vtime_epoch をそれぞれの基準点とし +// これらと現在の実経過時間、仮想経過時間の差が同期するよう調整する。 +// 高速走行モードにおいてはこれらは必要ないので特に参照しない。 +// +// 一定間隔で行うパフォーマンス測定は、前回計測時の実および仮想経過時間を +// 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); +} + +const std::string +TimeToStr(uint64 t) +{ + return TimeToStrF(t, FORMAT_FULL); +} + +static const std::string +SecToStr(uint64 t) +{ + return TimeToStrF(t, FORMAT_SEC); +} + +// +// スケジューラ +// + // コンストラクタ Scheduler::Scheduler() + : inherited("Scheduler") { - logname = "scheduler"; - devname = "Scheduler"; + for (int i = 0; i < perfq.Capacity(); i++) { + perfq.Enqueue(1000); + } - eventlist.clear(); + 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); } // デストラクタ @@ -41,35 +176,11 @@ Scheduler::~Scheduler() } // 初期化 -// ここで mpu_clock が(非ゼロに)初期化されるとこれ以降 putlog() が使える。 -// そのため、Scheduler デバイスをデバイスリストの先頭のほうに置くことで、 -// これ以降のオブジェクトの Init() は putlog() を使ってよいとできる。 -// BusErrDevice だけは Scheduler より先に作るが BusErrDevice に Init() は -// ないので (= 問題は起きないので)、よいことにする。 bool Scheduler::Init() { - const ConfigItem& item = gConfig->Find("mpu-clock"); - const std::string& val = item.AsString(); - if (val.empty()) { - item.Err(); - return false; - } - // 文字列を double に変換 - char *end; - errno = 0; - double f = strtod(val.c_str(), &end); - if (end == val.c_str() || end[0] != '\0' || errno == ERANGE) { - item.Err(); - return false; - } - // 設定の "mpu-clock" は MHz 単位だが、変数 mpu_clock は kHz 単位。 - mpu_clock = (int)(f * 1000); - // 1MHz 以下はエラー。実際 10MHz 以下でもいい気がするけど - if (mpu_clock < 1000) { - item.Err(); - return false; - } + // この時点でイベントの個数は確定しているはず(してなければならない) + monitor.SetSize(72, 10 + gEvents.size()); // 起動時引数指定の高速モード SetFullSpeed(gMainApp.fast_mode); @@ -81,15 +192,6 @@ Scheduler::Init() return true; } -// スケジューラを開始する -void -Scheduler::Run() -{ - std::unique_lock lock(cvmtx); - atomic_reqflag |= REQ_POWER_ON; - cv.notify_one(); -} - // スレッドエントリ関数 void * scheduler_run(void *arg) @@ -104,173 +206,418 @@ scheduler_run(void *arg) void Scheduler::ThreadRun() { - uint64 cycle; // 今回実行するサイクル数 - uint32 req; + // ここが rtime の基準点。 + // Stopwatch.Restart() は .Net 由来なので + // Restart() は経過時間をリセットしてからタイマーを開始の意。 + gRealtime.Restart(); + rtime = 0; + next_rtc_rtime = 31.25_msec; - // 最初の電源オンを待つ for (;;) { - std::unique_lock lock(cvmtx); - cv.wait(lock, [&] { return (atomic_reqflag != 0); }); - req = atomic_reqflag.load(); - // 終了 - if ((req & REQ_EXIT)) { + if (RunOff() == false) return; + if (RunOn() == false) + return; + } +} + +// 電源オフ時のスケジューラメインループ。 +// false ならこのスレッドの終了指示。 +bool +Scheduler::RunOff() +{ + uint32 oldreq; + + ispower = false; + + oldreq = -1; + for (;;) { + // RunOn() と違ってこっちは処理したフラグだけを下ろす。 + + // デバッグ表示も前回から変化した時だけ。 + uint32 req = atomic_reqflag.load(); + if (__predict_false(req != oldreq)) { + if (loglevel >= 2) { + PutlogReqFlag(req); + } + 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; } - // REQ_POWER_ON なら他のフラグを書き戻して抜ける。 - // REQ_{SYNC|FAST} は以降のメインループで処理するため。 - if ((req & REQ_POWER_ON)) { - req &= ~REQ_POWER_ON; - atomic_reqflag.store(req); - break; + + // 電源オン指示 + if ((atomic_reqflag & REQ_POWER_ON)) { + // 電源オン要求の場合 + + // 全デバイス電源オン + for (const auto& d : gDevices) { + if (!d->PowerOn()) { + return false; + } + } + + // 続いて全デバイスをリセット + for (const auto& d : gDevices) { + d->ResetHard(); + } + + // POWER_ON は処理したので下ろす。他のフラグは触らない。 + atomic_reqflag &= ~REQ_POWER_ON; + // 電源オンは RESET も兼ねる (RESET は RunOn() 内で行う) + atomic_reqflag |= REQ_RESET; + + return true; } + + // RTC はホスト時間で 32Hz (31.25msec) ごとにカウントアップするが、 + // usleep() はそこまでの精度がないことと、RunRTC() は前回カウント + // アップ時刻からの差で処理しており入力が多少ぶれても構わないので、 + // 適当にその周期の半分強で sleep してみる。 + usleep(16_msec / 1_usec); + + // RTC をホスト時間で進める。 + // (電源オンするまでは gRealTime は進み続けている) + rtime = GetRealTime(); + RunRTC(); } - // ここからは電源オン + __unreachable(); +} - // RTC 時刻だけは最初から必要。 - rtimestart = GetRealTime(); - rtc_last_clock = rtimestart; +// 電源オン時のスケジューラメインループ。 +// false ならこのスレッドの終了指示。 +bool +Scheduler::RunOn() +{ + 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(); - cycle = INT64_MAX; - mode = 0; for (;;) { - req = atomic_reqflag.exchange(0); - if (req) { - if ((req & REQ_SYNC)) { - // スケジューラを同期モードに - mode |= SCHED_SYNC; - } - if ((req & REQ_FAST)) { - // スケジューラを高速モードに - mode &= ~SCHED_SYNC; - } - if ((req & REQ_RUN)) { - // MPU が通常状態になった - mode &= ~SCHED_STOP; - } - if ((req & REQ_STOP)) { - // MPU が STOP 状態になった - mode |= SCHED_STOP; - } - if ((req & REQ_MODEMASK)) { - // mode がどれかにでも変わったら - static const char *modestr[] = { - "高速", - "高速 & STOP状態", - "通常", - "通常 & STOP状態", - }; - putlog(1, "モード変更 mode=%d (%s)", mode, modestr[mode]); - - // 基準時刻をリセット - rtimebase = ::GetRealTime(); - vtimebase = GetVirtTime(); + vtime = gMPU->GetVirtTime(); + + uint32 req = atomic_reqflag.exchange(0); + if (__predict_false(req != 0)) { - // 該当ビットを落としておく - req &= ~REQ_MODEMASK; + // デバッグ表示 + 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)) { // 電源オフ要求の場合 // 全デバイス電源オフ - gVM->DevicePowerOff(); + for (const auto& d : gDevices) { + if (!d->PowerOff()) { + return false; + } + } + + // GUI に通知 + UIMessage::Post(UIMessage::POWEROFF); + + return true; + } - req &= ~REQ_POWER_OFF; + if ((req & REQ_RESET)) { + // リセット要求の場合 + + // 全デバイスをリセット + for (const auto& d : gDevices) { + d->ResetHard(); + } + + req &= ~REQ_RESET; } if ((req & REQ_EXIT)) { // 終了要求 - // この for ブロックをいきなり抜けるだけなので - // もうフラグの値も関係ないのだが一応。 - req &= ~REQ_EXIT; - break; + return false; } - // 電源オン要求は来ないはず + // これいるかな? assertmsg(req == 0, "req=$%x", req); } - // RTC は常にホスト時刻で動いており - // 32Hz = 31.25msec ごとにパルスを入れる。 - while (::GetRealTime() > rtc_last_clock + 31.25_msec) { - gRTC->ClockIn(); - rtc_last_clock += 31.25_msec; - } - - // まず CPU を駆動する。MPU::Run() はデバイスアクセスなどによって - // スケジューラにイベントが登録されると、その命令の終了とともに - // 処理を打ち切って戻ってくる。 - // ここでイベントリストを調べ、その時点から最も早く発生するイベント - // までの必要サイクル数が、次のループでの駆動サイクル数になる。 - - // CPU 駆動 - uint32 outer; - outer = gMPU->Run(cycle); + // パフォーマンス表示 + 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 (;;) { - evcs.lock(); - if (eventlist.empty()) { - cycle = INT64_MAX; - break; - } - Event *e = eventlist.front(); - if (gMPU->total_cycle() < e->cycle) { - // まだ時刻に到達していない - cycle = e->cycle - gMPU->total_cycle(); - break; - } - - // 到達したのでこのイベントをリストから削除 - e->active = false; - eventlist.pop_front(); - evcs.unlock(); + 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->putlog(3, "イベント '%s' 時刻到達", e->GetName().c_str()); - ((e->dev)->*(e->func))(e->code); + e->dev->putlogf(3, lstr("イベント '%s' 時刻到達", + e->GetName().c_str())); + ((e->dev)->*(e->func))(*e); // 同時刻のイベントがあるかも知れないのでなくなるまで調べる } - evcs.unlock(); - // 同期モードなら実時間調整。 - // あるいは高速モードであってもストップ状態なら実時間駆動する。 - if (mode != 0) { - // 通常モード - uint64 vtime; - uint64 rtime; - - // 基準時からの経過時間 - vtime = GetVirtTime() - vtimebase; - rtime = GetRealTime() - rtimebase; - - if (vtime > rtime) { - // 仮想時間のほうが進んでいれば、スリープして待つ - uint64 diff = vtime - rtime; - struct timespec ts; - ts.tv_sec = diff / (1000 * 1000 * 1000); - ts.tv_nsec = diff % (1000 * 1000 * 1000); - nanosleep(&ts, NULL); - } else { - // 実時間のほうが進んでいれば、間に合ってない + // RTC をホスト時間で進める + // (同期イベントによって sleep したかもしれないので、その後がよかろう) + RunRTC(); - // XXX ここで間引き運転とか - } + // CPU を駆動 + new_cpumode = gMPU->Run(delta_vtime); + + // CPU 側の走行状態が変わったら、このループに通知 + if (__predict_false(((mode ^ new_cpumode) & SCHED_CPU_MASK))) { + atomic_reqflag |= REQ_CPU_MODE; } + } + __unreachable(); +} - // CPU の STOP 状態をこっちのモードに反映。 - // このために SCHED_STOP と CPU_REQ_STOP は同じビット位置にしてある。 - // CPU の HALT はスケジューラ的には STOP と同じ扱いでいい。 - if (((mode ^ outer) & SCHED_STOP)) { - // 状態変更要求 - if ((outer & (CPU_REQ_STOP | CPU_REQ_HALT)) != 0) { - atomic_reqflag |= REQ_STOP; - } else { - atomic_reqflag |= REQ_RUN; +// 同期イベント +void +Scheduler::SyncCallback(Event& ev) +{ + Sync(); + + // 次回のイベントを再登録 + ev.Start(); +} + +// 同期処理 +// ここで仮想時間を実時間と同期させる。 +// スリープしすぎた実時間を返す。 +uint64 +Scheduler::Sync() +{ + 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 ここで間引き運転とか } } + return over; +} + +// RTC は常にホスト時刻で動いており +// 32Hz = 31.25msec ごとにパルスを入れる。 +// VM の電源がオフでも実行する。 +void +Scheduler::RunRTC() +{ + while (rtime >= next_rtc_rtime) { + gRTC->ClockIn(); + next_rtc_rtime += 31.25_msec; + } } // 必要ならスレッドの終了を指示して完了まで待つ。 @@ -278,22 +625,113 @@ void Scheduler::Terminate() { if (thread_created) { - atomic_reqflag |= REQ_EXIT; + RequestExit(); + Wait(); + } +} + +// スレッドの完了まで待つ。 +void +Scheduler::Wait() +{ + if (thread_created) { pthread_join(thread, NULL); thread_created = false; } } +// スレッド終了を指示する。 +void +Scheduler::RequestExit() +{ + atomic_reqflag |= REQ_EXIT; +} + // 動作モードを設定する。 // true なら高速モード、false なら同期モード。 void Scheduler::SetFullSpeed(bool enable) { if (enable) { - atomic_reqflag |= REQ_FAST; + new_usermode = 0; + } else { + new_usermode = SCHED_SYNC; + } + atomic_reqflag |= REQ_USER_MODE; +} + +// ブートページモードを設定する。 +// 戻り値は現在の動作モードがブートモード(ROM)なら true。 +// (VM::SwitchBootPage から呼ぶので個別には呼ばないこと) +void +Scheduler::RequestBootPageMode(bool isrom) +{ + if (isrom) { + new_bootmode = SCHED_BOOT; + } else { + new_bootmode = 0; + } + atomic_reqflag |= REQ_BOOT_MODE; +} + +// キー入力中かどうかを設定する。 +// true ならキー入力中、false なら入力中でない。 +void +Scheduler::RequestKeyPressed(bool pressed) +{ + if (pressed) { + new_keymode = SCHED_KEY; } else { - atomic_reqflag |= REQ_SYNC; + new_keymode = 0; } + atomic_reqflag |= REQ_KEY_MODE; +} + +// REQ_* フラグのデバッグ表示 +void +Scheduler::PutlogReqFlag(uint32 req) const +{ + static const char * const cpumode_str[] = { + "Normal", + "STOP", + "HALT", + "HALTXXX", + }; + char buf[128]; + int len = 0; + + buf[0] = '\0'; + +#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"); + } + + if (buf[0] != '\0') { + buf[0] = '<'; + strlcat(buf, ">", sizeof(buf)); + } + putlogn("%sreq=$%04x%s", (ispower ? "" : "Off "), req, buf); } // 指定のイベントを開始する。 @@ -303,48 +741,31 @@ Scheduler::SetFullSpeed(bool enable) void Scheduler::StartEvent(Event *newev) { - bool inserted = false; bool updated = false; - // 相対 time から絶対 cycle を計算 - newev->cycle = gMPU->total_cycle() + Vtime2Cycle(newev->time); + // time は仮想相対時間 [nsec]、vtime は仮想絶対時刻 [nsec] + newev->vtime = gMPU->GetVirtTime() + newev->time; evcs.lock(); - // すでにあれば削除 + // すでにあれば削除して... if (newev->active) { - for (auto it = eventlist.begin(); it != eventlist.end(); ++it) { - Event *e = *it; - if (e == newev) { - eventlist.erase(it); - updated = true; - break; - } - } - } - // ソートされているところに自身を挿入 - for (auto it = eventlist.begin(); it != eventlist.end(); ++it) { - Event *e = *it; - if (newev->cycle <= e->cycle) { - // このイベントの前に入れる - eventlist.insert(it, newev); - inserted = true; - break; - } - } - if (inserted == false) { - // 最後に追加 - eventlist.push_back(newev); + updated = eventq.Erase(newev); } + // 自身を追加 + eventq.push(newev); newev->active = true; - evcs.unlock(); - gMPU->Release(); + // 先頭なら再スケジュールのため CPU 実行をここで打ち切り + if (eventq.top() == newev) { + gMPU->Release(); + } + evcs.unlock(); - newev->dev->putlog(3, "イベント '%s' %s %d.%03d usec 後", + newev->dev->putlogf(3, lstr("イベント '%s' %s %d.%03d usec 後", newev->GetName().c_str(), updated ? "更新" : "開始", (int)(newev->time / 1000), - (int)(newev->time % 1000)); + (int)(newev->time % 1000))); } // 指定のイベントを停止する。 @@ -355,16 +776,9 @@ Scheduler::StopEvent(Event *event) bool found = false; evcs.lock(); - for (auto it = eventlist.begin(); it != eventlist.end(); ++it) { - Event *e = *it; - if (e == event) { - e->active = false; - eventlist.erase(it); - found = true; - break; - } - } + found = eventq.Erase(event); evcs.unlock(); + event->active = false; // イベントを停止した場合は MPU の実行中断はしなくてよい。 // 1msec 後にイベントを追加した後、やっぱりそのイベントを取り消した @@ -372,128 +786,115 @@ Scheduler::StopEvent(Event *event) // そのまま 1msec 走って問題ない。 if (found) { - event->dev->putlog(3, "イベント '%s' 停止", event->GetName().c_str()); + event->dev->putlogf(3, lstr("イベント '%s' 停止", + event->GetName().c_str())); } } -// 経過時間 t を文字列にして返す。 -static const std::string -TimeToStr(uint64 t) +void +Scheduler::MonitorUpdate(Monitor *, TextScreen& screen) { - char buf[32]; - char *p; - size_t len; - int n; + int x, y; - uint ns = t % 1000; - t /= 1000; - uint us = t % 1000; - t /= 1000; - uint ms = t % 1000; - t /= 1000; - uint s = t % 60; - t /= 60; - uint m = t % 60; - t /= 60; - uint h = t; + screen.Clear(); + x = 0; + y = 0; - p = buf; - len = sizeof(buf); - if (h) { - n = snprintf(p, len, "%d:%02d:%02d", h, m, s); - p += n; - len -= n; - } else if (m) { - n = snprintf(p, len, "%d:%02d", m, s); - p += n; - len -= n; + int clock_khz = gMPU->GetClockSpeed(); + + screen.Print(0, y, "Mode: 0x%04x (", mode); + if ((mode & SCHED_SYNC)) { + screen.Puts(14, y, TA::On, "Sync"); } else { - n = snprintf(p, len, "%d", s); - p += n; - len -= n; + screen.Puts(14, y, TA::Off, "Full"); } - n = snprintf(p, len, ".%03d'%03d'%03d", ms, us, ns); - p += n; - len -= n; - - return std::string(buf, p - buf); -} - -// モニターサイズ取得 -nnSize -Scheduler::GetMonitorSize() -{ - // 本当は Init() 完了時点でイベント個数は確定しているはずだけど、 - // そのタイミングで一度だけ呼ばれるフックが今の所ない。 - return nnSize(68, 11 + gEvents.size()); -} + 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, ")"); + 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()); -void -Scheduler::MonitorUpdate(TextScreen& monitor) -{ - int x, y; + screen.Print(0, y++, "Performance : %4d%%", perf_counter); - monitor.Clear(); - x = 0; + x = 44; y = 0; - - monitor.Print(0, y++, "Mode: %s %s", - (mode & SCHED_SYNC) ? "Sync" : "Full", - (mode & SCHED_STOP) ? "STOP" : "Run "); - monitor.Print(0, y++, "Req: %08x", (uint32)atomic_reqflag); - monitor.Print(0, y++, "MPU Speed: %d.%03dMHz", - (mpu_clock / 1000), (mpu_clock % 1000)); - uint64 cycle = gMPU->total_cycle(); - uint64 vt = GetVirtTime(); - uint64 rt = GetRealTime(); - uint64 relvt = vt - vtimebase; - uint64 relrt = rt - rtimebase; - // RealTime は実時間なのでスケジューラ開始からの時間に変換 - rt -= rtimestart; - monitor.Print(0, y++, "Total Real Time: %18s", TimeToStr(rt).c_str()); - monitor.Print(0, y++, " Virtual Time: %18s", TimeToStr(vt).c_str()); - double ratio = (double)vt / rt * 100; - monitor.Print(0, y++, " Ratio : %3d.%01d%%", - (int)ratio, ((int)(ratio * 10) % 10)); - monitor.Print(0, y++, "Moment Real Time: %18s", TimeToStr(relrt).c_str()); - monitor.Print(0, y++, " Virtual Time: %18s", TimeToStr(relvt).c_str()); - // XXX 移動平均をとる - double relratio = (double)relvt / relrt * 100; - monitor.Print(0, y++, " Ratio : %3d.%01d%%", - (int)relratio, ((int)(relratio * 10) % 10)); + 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 - // 0123456789012345678901234567890123456789012345678901234567890123456 - // Set Time Remain Time Description Code - // 3.123'456'789 3.123'456'789 0123456789012345678901234 $01234567 + // 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"); y++; - monitor.Puts(x, y, "Set Time"); - monitor.Puts(x + 16, y, "Remain Time"); - monitor.Puts(x + 32, y, "Description"); - monitor.Puts(x + 58, y, "Code"); - y++; + for (const auto& e : gEvents) { uint64 rem; TA attr; if (e->active) { attr = TA::Normal; - rem = Cycle2Vtime(e->cycle - cycle); + if (e->vtime > vtime) { + rem = e->vtime - vtime; + } else { + // XXX 実際には起きないけど、この表示をするにあたって + // スケジューラスレッドと一切調停していないので、仮想時刻が + // e->cycle (イベント発生時刻) をすぎていることがある。 + // すぎてるので残り時間 0 と表示しておく。 + rem = 0; + } } else { attr = TA::Disable; rem = 0; } - monitor.Print(x, y++, attr, - "%3u.%03u'%03u'%03u %3u.%03u'%03u'%03u %-25s $%08x", - (uint)(e->time / (1000 * 1000 * 1000)), - (uint)((e->time / 1000 / 1000) % 1000), - (uint)((e->time / 1000) % 1000), - (uint)(e->time % 1000), - (uint)(rem / (1000 * 1000 * 1000)), - (uint)((rem / 1000 / 1000) % 1000), - (uint)((rem / 1000) % 1000), - (uint)(rem % 1000), - e->GetName().c_str(), - e->code); + screen.Print(x, y++, attr, + "%13s %13s %-30s", + SecToStr(e->time).c_str(), + SecToStr(rem).c_str(), + e->GetName().c_str()); } }