/*
 *	cliptimr.c
 *
 *	ユーティリティ：拡張タイマ
 *
 *	CLiP - Common Library for P/ECE
 *	Copyright (C) 2001-2016 Naoyuki Sawa
 *
 *	* Wed Nov 18 21:37:53 JST 2015 Naoyuki Sawa
 *	- 1st リリース。
 *	- 「ユーティリティ：拡張タイマ」を、(clippce.h,clippce.c)⇒(cliptimr.h,cliptimr.c)へ移動しました。
 *	  移動する事にした理由は、timer_start()とtimer_stop()を、Win32にも対応したからです。
 *	  (clippce.h,clippce.c)はP/ECE専用でありWin32プロジェクトには含められないので、別のファイルへ分離しました。
 *	* Mon Oct 03 21:41:16 JST 2016 Naoyuki Sawa
 *	- timer_proc()の遅れが発生し易かった問題を修正しました。
 *	  詳細は、以下の通りです。
 *	- これまで、タイマ割り込み一回あたりの処理回数の上限を(TIMER_DT_MAX=10)としていました。
 *	  これで十分だろうと思っていたのですが、Win32環境で、周期処理の遅れが発生する問題が発生しました。
 *	  具体的には、LEDシーケンサ処理を「TimMgr_AddFunc(pTimMgr,TimMgrCh_1f,〜)」で登録し、サウンド再生と並行して再生すると、LEDシーケンサ処理が遅れて行くという問題です。
 *	  調査した所、LEDシーケンサ処理の呼び出しが遅れており、原因をたどると、当モジュール(cliptimr.c)からcliptimm.cへの、TimMgr_Intr()呼び出しが遅れているのが原因でした。
 *	- 試しに、一時的にTIMER_DT_MAXを100に増やしてみたところ、LEDシーケンサ処理は無くなりました。
 *	  つまり、タイマ割り込み(厳密にはWin32環境ではtimer_thread_proc()でのタイマイベント監視)の誤差が、10ミリ秒を超えているという事です。
 *	  計測してみた所、特にサウンドの再生時等で誤差が大きくなる事が多いようでした。
 *	  おそらく、そのタイミングでWin32内部(又はDxLib内部)でtimer_thread_proc()よりも高い優先度のスレッドが長時間動作しているか、又は、コンテキストスイッチに時間が掛かっているのだと思います。
 *	- 簡単な解決策としては、前述の通り、TIMER_DT_MAXを増やす事ですが、それは行いたくありませんでした。
 *	  P/ECEにおいても、Win32においても、timer_proc()が一連の割り込み禁止区間の粒度となりますので、一回のtimer_proc()の処理時間をあまり長くしたくないからです。
 *	  そこで、別の解決策として、「今回の経過時間」と「蓄積した残り処理回数」に分けて管理する事にしました。
 *	  これまでは、「今回の経過時間」だけを管理しておりそれを上限10に切り詰めていたのですが、今後は、「今回の経過時間」を10に切り詰めるために余った経過時間は、「蓄積した残り処理回数」に加算しておいて次回以降に(余裕が有れば)処理します。
 *	  こうする事で、一回の処理回数の条件は10のままで、それよりも大きな遅れが発生した場合は複数回に分散して処理出来るようになりました。
 *	  ただし、あまりにも長時間の遅れが発生した場合は、ブーストが掛かり過ぎないように、蓄積した残り処理回数の上限は1000とする事にしました。
 *	- 今回の変更の元々の目的は、前述の通り、Win32環境で発生した問題の解決が目的なのですが、P/ECEにも適用する事にしました。
 *	  P/ECEでも、同様の問題は発生し得ると思うからです。
 *	  今回の変更の影響を、P/ECE環境でまだ十分に検証出来ていないので、万一、P/ECE環境で問題が生じた場合は、条件コンパイルでP/ECE環境だけ元の処理に戻す事を検討して下さい。
 */
#include "clip.h"

/****************************************************************************
 *	ユーティリティ：拡張タイマ
 ****************************************************************************/

//{{2014/12/28変更:拡張タイマテーブルを、固定割り当て⇒動的割り当てに変更しました。
//typedef struct _TIMER {
//	int count;			/* 次回タイムアウトまでの残り時間[ms](0=停止) */
//	int interval;			/* タイムアウト間隔[ms] */
//	void (*callback)(void* context);/* タイムアウト時に呼び出す、コールバック関数 */
//	void* context;			/* コールバック引数 */
//} TIMER;
//static TIMER timers[16/*調整可*/];
///*--------------------------------------------------------------------------*/
//static void timer_proc() {
//	int id;
//	TIMER* timer;
//	/* 各タイマについて... */
//	for(id = 0; id < ARRAY_SIZE(timers); id++) {
//		timer = &timers[id];
//		/* このタイマが動作中ならば残り時間を減らし、タイムアウトしたら... */
//		if((timer->count)/*動作中?*/ && (!--timer->count)/*タイムアウト?*/) {
//			/* 先に、次回タイムアウトまでの残り時間を初期化しておきます。
//			 * コールバック関数内でタイマを停止した場合、上書きされます。 */
//			timer->count = timer->interval;
//			/* コールバック関数を呼び出します。 */
//			timer->callback(timer->context);
//		}
//	}
//}
///*--------------------------------------------------------------------------*/
//int timer_start(int interval, void (*callback)(void* context), void* context) {
//	int id;
//	TIMER* timer;
//	if((interval <= 0) || (!callback)) {
//		DIE();	/* パラメータ不正 */
//	}
//ENTER_CS;
//	/* 停止中のタイマを探します。 */
//	for(id = 0; id < ARRAY_SIZE(timers); id++) {
//		timer = &timers[id];
//		if(!timer->count) {
//			break;
//		}
//	}
//	if(id == ARRAY_SIZE(timers)) {
//		DIE();	/* 空きタイマ無し */
//	}
//	/* タイマを初期化します。 */
//	timer->count = timer->interval = interval;
//	               timer->callback = callback;
//	               timer->context  = context ;
//LEAVE_CS;
//	/* 拡張タイマドライブ用の1msコールバックを起動します。
//	 * 実装を簡単にするため、二回目以降も毎回再起動することにします。 */
//	pceTimerSetCallback(0/*変更可*/, PCE_TT_PERIODIC, 1, timer_proc);
//	return id + 1/*タイマIDは1ベース*/;
//}
///*--------------------------------------------------------------------------*/
//void timer_stop(int id) {
//	TIMER* timer;
//	id--; /*タイマIDは1ベース*/
//	if((id < 0) || (id > ARRAY_SIZE(timers) - 1)) {
//		DIE();	/* タイマID不正 */
//	}
//	timer = &timers[id];
//ENTER_CS;
//	if(!timer->count) {
//		DIE();	/* タイマ停止中 */
//	}
//	/* タイマを停止します。 */
//	timer->count = 0;
//LEAVE_CS;
//}
//↓2014/12/28変更:拡張タイマテーブルを、固定割り当て⇒動的割り当てに変更しました。
typedef struct _ST_TIMER {
	int	count;				//+ 0,4 次回タイムアウトまでの残り時間[ms](0=停止)
	int	interval;			//+ 4,4 タイムアウト間隔[ms]
	void	(*callback)(void* context);	//+ 8,4 タイムアウト時に呼び出す、コールバック関数
	void*	context;			//+12,4 コールバック引数
} ST_TIMER;					//=16
static int      SIZE_timer;			//拡張タイマテーブルの要素数
static ST_TIMER* TBL_timer/*[SIZE_timer]*/;	//拡張タイマテープル
/*--------------------------------------------------------------------------*/
//{{2015/09/16変更:timer_proc()が、経過時間に応じた処理回数分まとめて処理するように、変更しました。
//static void timer_proc() {
//	int id;
//	ST_TIMER* timer;
//	//各拡張タイマについて…
//	// - コールバック関数内でtimer_start()が呼ばれた場合、TBL_timerとSIZE_timerが変化する可能性があるので、
//	//   ループ開始前にTBL_timer,SIZE_timerを取得しておくことはできず、ループ毎にグローバル変数を参照する。
//	for(id = 0; id < SIZE_timer; id++) {
//		timer = &TBL_timer[id];
//		//この拡張タイマが動作中ならば残り時間を減らし、タイムアウトしたら…
//		if((timer->count)/*動作中?*/ && (!--timer->count)/*タイムアウト?*/) {
//			//先に、次回タイムアウトまでの残り時間を初期化しておきます。
//			timer->count = timer->interval;
//			//↑この処理順序は必須です。理由は以下の通りです:
//			//│- コールバック関数内でtimer_stop()が呼ばれた場合、timer->countが0に上書きされます。
//			//│  timer_stop()が呼ばれ、さらにtimer_start()が呼ばれて再利用される可能性も有ります。
//			//↓  どちらの場合も、この処理順序で、問題なく処理できます。
//			//コールバック関数を呼び出します。
//			(*timer->callback)(timer->context);
//		}
//	}
//}
//↓2015/09/16変更:timer_proc()が、経過時間に応じた処理回数分まとめて処理するように、変更しました。
// * Wed Sep 16 01:23:07 JST 2015 Naoyuki Sawa
// - timer_proc()が、経過時間に応じた処理回数分まとめて処理するように、変更しました。
// - このように変更したきっかけは、ストリームコールバックや標準エラー出力(USB)を使うと、結構タイマ割り込みが飛ぶ事に気付いたからです。(ストリームコールバックと標準エラー出力(USB)のどちらが主な要因かは未調査です。)
//   例えば、TAPシーケンサはタイマで再生時間のを計測して自然ループや自然停止のコールバックを行うのですが、実際に音の再生がループするよりも自然ループのコールバックがだんだん遅れる事に気付きました。
//   音の再生はミキシング割り込み(DMA割り込み)駆動であり、タイマ割り込みよりも正確であるはずなので、タイマ割り込みの方が遅れているのだと判断しました。
// - おそらく、ミキシング割り込みが1[ms]以上かかった時に、タイマ割り込みが飛んでいるのだと思いました。
//   そこで、タイマ割り込みが飛んだ後は、経過時間に応じた処理回数分まとめて処理する事にしました。
//   この修正を行った後は、TAPシーケンサの自然ループのコールバックが遅れなくなった事を、動作確認しました。
// - 本来この修正は、timer_proc()で対応するよりも、タイマ割り込みそのもの(timer_isr())で行う方が望ましいのですが、出来ませんでした。
//   timer_isr()は、スタックフレームの構成を仕様化しているため、経過時間に応じた処理回数分まとめて処理するためのループ処理を入れられないからです。
//   当ソースの後ろの方に有る、new_timer_isrの実装を見れば、その中にループを入れられない事が理解出来ると思います。
// * Mon Oct 03 21:41:16 JST 2016 Naoyuki Sawa
// - timer_proc()の遅れが発生し易かった問題を修正しました。
//   詳細は、以下の通りです。
// - これまで、タイマ割り込み一回あたりの処理回数の上限を(TIMER_DT_MAX=10)としていました。
//   これで十分だろうと思っていたのですが、Win32環境で、周期処理の遅れが発生する問題が発生しました。
//   具体的には、LEDシーケンサ処理を「TimMgr_AddFunc(pTimMgr,TimMgrCh_1f,〜)」で登録し、サウンド再生と並行して再生すると、LEDシーケンサ処理が遅れて行くという問題です。
//   調査した所、LEDシーケンサ処理の呼び出しが遅れており、原因をたどると、当モジュール(cliptimr.c)からcliptimm.cへの、TimMgr_Intr()呼び出しが遅れているのが原因でした。
// - 試しに、一時的にTIMER_DT_MAXを100に増やしてみたところ、LEDシーケンサ処理は無くなりました。
//   つまり、タイマ割り込み(厳密にはWin32環境ではtimer_thread_proc()でのタイマイベント監視)の誤差が、10ミリ秒を超えているという事です。
//   計測してみた所、特にサウンドの再生時等で誤差が大きくなる事が多いようでした。
//   おそらく、そのタイミングでWin32内部(又はDxLib内部)でtimer_thread_proc()よりも高い優先度のスレッドが長時間動作しているか、又は、コンテキストスイッチに時間が掛かっているのだと思います。
// - 簡単な解決策としては、前述の通り、TIMER_DT_MAXを増やす事ですが、それは行いたくありませんでした。
//   P/ECEにおいても、Win32においても、timer_proc()が一連の割り込み禁止区間の粒度となりますので、一回のtimer_proc()の処理時間をあまり長くしたくないからです。
//   そこで、別の解決策として、「今回の経過時間」と「蓄積した残り処理回数」に分けて管理する事にしました。
//   これまでは、「今回の経過時間」だけを管理しておりそれを上限10に切り詰めていたのですが、今後は、「今回の経過時間」を10に切り詰めるために余った経過時間は、「蓄積した残り処理回数」に加算しておいて次回以降に(余裕が有れば)処理します。
//   こうする事で、一回の処理回数の条件は10のままで、それよりも大きな遅れが発生した場合は複数回に分散して処理出来るようになりました。
//   ただし、あまりにも長時間の遅れが発生した場合は、ブーストが掛かり過ぎないように、蓄積した残り処理回数の上限は1000とする事にしました。
// - 今回の変更の元々の目的は、前述の通り、Win32環境で発生した問題の解決が目的なのですが、P/ECEにも適用する事にしました。
//   P/ECEでも、同様の問題は発生し得ると思うからです。
//   今回の変更の影響を、P/ECE環境でまだ十分に検証出来ていないので、万一、P/ECE環境で問題が生じた場合は、条件コンパイルでP/ECE環境だけ元の処理に戻す事を検討して下さい。
#define TIMER_DT_MAX	10	//経過時間に応じた処理回数の上限(調整可)
//{{2016/10/03追加:timer_proc()の遅れが発生し易かった問題を修正しました。
#define TIMER_DT_SUM_MAX	1000	//蓄積した残り処理回数の上限(調整可)
//}}2016/10/03追加:timer_proc()の遅れが発生し易かった問題を修正しました。
static void timer_proc() {
	static int t0;	//前回の時刻[ms]。0ならば初回(=未初期化)を意味する。約49日で一周するが実用上問題無い。もし一周して0に一致し再初期化が発生した場合もそれによる誤差は1[ms]程度だろう。
//{{2016/10/03追加:timer_proc()の遅れが発生し易かった問題を修正しました。
	static int dt_sum;	//蓄積した残り処理回数[ms]
//}}2016/10/03追加:timer_proc()の遅れが発生し易かった問題を修正しました。
	int t1, dt, id;
#if     defined(PIECE)
	if(!t0) { t0 = pceTimerGetCount(); }	//前回の時刻が初回(未初期化)ならば現在の時刻を取得する。
	          t1 = pceTimerGetCount();	//現在の時刻を取得する。
#elif   defined(WIN32)
	if(!t0) { t0 = timeGetTime(); }		//前回の時刻が初回(未初期化)ならば現在の時刻を取得する。
	          t1 = timeGetTime();		//現在の時刻を取得する。
#else //defined(WIN32)
#error  このファイルは、P/ECE,又は,Win32プロジェクト専用です。
#endif//defined(WIN32)
	dt = t1 - t0;				//経過時間を求める。(=0となる可能性も有る事に注意せよ。)
	     t0 = t1;				//前回の時刻を更新する。
//{{2016/10/03変更:timer_proc()の遅れが発生し易かった問題を修正しました。
//	if((unsigned)dt > TIMER_DT_MAX) { dt = TIMER_DT_MAX; }	//経過時間が制限を超過したら切り詰める。割り込みが極端に長時間禁止された直後に以下のループが回り過ぎる事を防ぐ安全対策です。
//↓2016/10/03変更:timer_proc()の遅れが発生し易かった問題を修正しました。
	dt_sum += dt;								//蓄積した残り処理回数に、今回の経過時間を加算する。厳密には飽和演算を行うべきだが、実際にオーバーフローする事は無いだろうから、飽和演算を行わなくても大丈夫だろう。
	if((unsigned)dt_sum > TIMER_DT_SUM_MAX) { dt_sum = TIMER_DT_SUM_MAX; }	//蓄積した残り処理回数が制限を超過したら切り詰める。
	dt = dt_sum;								//┐
	if((unsigned)dt > TIMER_DT_MAX) { dt = TIMER_DT_MAX; }			//├TIMER_DT_MAXを上限として今回の処理回数を決定し、今回の処理する回数分を蓄積した残り処理回数から差し引く。
	dt_sum -= dt;								//┘
//}}2016/10/03変更:timer_proc()の遅れが発生し易かった問題を修正しました。
	while(--dt >= 0) {	//経過時間に応じた処理回数分ループする。経過時間0ならば一度もループしない可能性も有る。
		//各拡張タイマについて…
		// - コールバック関数内でtimer_start()が呼ばれた場合、TBL_timerとSIZE_timerが変化する可能性があるので、
		//   ループ開始前にTBL_timer,SIZE_timerを取得しておくことはできず、ループ毎にグローバル変数を参照する。
		for(id = 0; id < SIZE_timer; id++) {
			ST_TIMER* timer = &TBL_timer[id];
			//この拡張タイマが動作中ならば残り時間を減らし、タイムアウトしたら…
			if((timer->count)/*動作中?*/ && (!--timer->count)/*タイムアウト?*/) {
				//先に、次回タイムアウトまでの残り時間を初期化しておきます。
				timer->count = timer->interval;
				//↑この処理順序は必須です。理由は以下の通りです:
				//│- コールバック関数内でtimer_stop()が呼ばれた場合、timer->countが0に上書きされます。
				//│  timer_stop()が呼ばれ、さらにtimer_start()が呼ばれて再利用される可能性も有ります。
				//↓  どちらの場合も、この処理順序で、問題なく処理できます。
				//コールバック関数を呼び出します。
				(*timer->callback)(timer->context);
			}
		}
	}
}
//}}2015/09/16変更:timer_proc()が、経過時間に応じた処理回数分まとめて処理するように、変更しました。
/*--------------------------------------------------------------------------*/
#if     defined(WIN32)
// * Wed Nov 18 21:37:53 JST 2015 Naoyuki Sawa
// - timeSetEvent()にTIME_CALLBACK_FUNCTIONを指定して直接timer_proc()を呼び出させるのではなく、timeSetEvent()にはTIME_CALLBACK_EVENT_SETを指定してイベントをセットさせ、別スレッドで監視してtimer_proc()を呼び出す方法とした。
//   こうした理由は、timeSetEvent()にTIME_CALLBACK_FUNCTIONを指定した場合、負荷や排他制御等の理由で長時間コールバックが停止した場合、再開したときに経過した時間分まとめてコールバックが発生するからだ。(timeSetEvent()の仕様)
// - たとえば、1ms間隔のtimeSetEvent()に対して、コールバックを1秒間ロックした場合、ロックを解除した直後に1000回コールバックが発生してしまう。
//   そのような動作では、アプリケーションが任意の上限(=TIMER_DT_MAX)を設けて極端に多いコールバックを制限する事が出来ない。
// - そこで、timeSetEvent()にはTIME_CALLBACK_EVENT_SETを指定してイベントをセットさせ、別スレッドで監視してtimer_proc()を呼び出す方法とした。
//   この方法ならば、長時間処理がロックしたしていたとしても、複数回のイベントセットはまとめて一回と見なされる。
//   その後、アプリケーションが明示的にtimeGetTime()で時間を取得し、任意の上限を設けて(必要ならば)複数回のコールバックを実行する事が出来る。
static unsigned __stdcall timer_thread_proc(void* arg) {
	HANDLE hEvent = arg;
	for(;;) {
		if(WaitForSingleObject(hEvent, INFINITE) != WAIT_OBJECT_0) { DIE(); }
ENTER_CS;
		timer_proc();
LEAVE_CS;
	}
}
#endif//defined(WIN32)
/*--------------------------------------------------------------------------*/
int timer_start(int interval, void (*callback)(void* context), void* context) {
	int id;
	ST_TIMER* timer;
	if((interval <= 0) || (!callback)) { DIE(); }	//引数不正
ENTER_CS;
	//初回ならば、拡張タイマドライブ用の1msコールバックを起動します。
	if(!SIZE_timer) {
#if     defined(PIECE)
		pceTimerSetCallback(0/*変更可*/, PCE_TT_PERIODIC, 1, timer_proc);
#elif   defined(WIN32)
		HANDLE hEvent, hThread;
		hEvent = CreateEvent(
			NULL/*lpEventAttributes*/,
			FALSE/*bManualReset=自動リセットオブジェクト*/,
			FALSE/*bInitialState=非シグナル状態*/,
			NULL/*lpName*/);
		if(!hEvent) { DIE(); }
		hThread = (HANDLE)_beginthreadex(
			NULL/*security*/,
			0/*stack_size*/,
			timer_thread_proc/*start_address*/,
			hEvent/*arglist*/,
			0/*initflag*/,
			NULL/*thrdaddr*/);
		if(!hThread) { DIE(); }
		SetThreadPriority(hThread, GetThreadPriority(GetCurrentThread()) + 1);	//timer_start()を呼び出したスレッド(多分メインスレッドだろう)よりも、timer_thread_proc()のスレッドの優先度を上げておく。timer_thread_proc()のスレッドは擬似的なタイマ割り込みに相当するスレッドなので、メインスレッドよりも優先度を高くするのが適切である。
		CloseHandle(hThread);	//スレッドハンドルはもう不要なので閉じて良い。
		if(!timeSetEvent(
			1/*uDelay*/,
			0/*uResolution*/,
			(LPTIMECALLBACK)hEvent/*lpTimeProc*/,
			0/*dwUser*/,
			TIME_PERIODIC|TIME_CALLBACK_EVENT_SET/*fuEvent*/)) { DIE(); }
#else //defined(WIN32)
#error  このファイルは、P/ECE,又は,Win32プロジェクト専用です。
#endif//defined(WIN32)
	}
	//拡張タイマテーブルの中から、停止中の拡張タイマを探します。
	for(id = 0; id < SIZE_timer; id++) {
		timer = &TBL_timer[id];
		if(!timer->count) { break; }
	}
	//拡張タイマテーブルの中に、停止中の拡張タイマが無かったら…
	if(id == SIZE_timer) {
		//拡張タイマテーブルを拡張します。
		TBL_timer = realloc(TBL_timer, ++SIZE_timer * sizeof(ST_TIMER));
		if(!TBL_timer) { DIE(); }	//メモリ不足
	}
	//拡張タイマを初期化します。
	timer = &TBL_timer[id];
	timer->count = timer->interval = interval;
	               timer->callback = callback;
	               timer->context  = context ;
LEAVE_CS;
	return id + 1;	//拡張タイマIDは1ベース
}
/*--------------------------------------------------------------------------*/
void timer_stop(int id) {
	ST_TIMER* timer;
	id--;		//拡張タイマIDは1ベース
ENTER_CS;
	if((unsigned)id >= (unsigned)SIZE_timer) { DIE(); }	//拡張タイマID不正
	timer = &TBL_timer[id];
	if(!timer->count) { DIE(); }	//既に停止したタイマを二重停止しようとした。
	//拡張タイマを停止します。
	timer->count = 0;
LEAVE_CS;
}
//}}2014/12/28変更:拡張タイマテーブルを、固定割り当て⇒動的割り当てに変更しました。
