diff --git a/AppleWinExpress2008.vcproj b/AppleWinExpress2008.vcproj
index 369aeecf..5fd2985e 100644
--- a/AppleWinExpress2008.vcproj
+++ b/AppleWinExpress2008.vcproj
@@ -757,6 +757,14 @@
RelativePath=".\source\Speech.h"
>
+
+
+
+
diff --git a/AppleWinExpress2019.vcxproj b/AppleWinExpress2019.vcxproj
index 004429d6..d078c268 100644
--- a/AppleWinExpress2019.vcxproj
+++ b/AppleWinExpress2019.vcxproj
@@ -96,6 +96,7 @@
+
@@ -184,6 +185,7 @@
Create
Create
+
NotUsing
diff --git a/AppleWinExpress2019.vcxproj.filters b/AppleWinExpress2019.vcxproj.filters
index f6503852..8dc47688 100644
--- a/AppleWinExpress2019.vcxproj.filters
+++ b/AppleWinExpress2019.vcxproj.filters
@@ -199,6 +199,9 @@
Source Files\Disk
+
+ Source Files\Emulator
+
@@ -483,6 +486,9 @@
Source Files\Emulator
+
+ Source Files\Emulator
+
diff --git a/source/Applewin.cpp b/source/Applewin.cpp
index 7449496b..96141144 100644
--- a/source/Applewin.cpp
+++ b/source/Applewin.cpp
@@ -53,6 +53,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#ifdef USE_SPEECH_API
#include "Speech.h"
#endif
+#include "SynchronousEventManager.h"
#include "Video.h"
#include "RGBMonitor.h"
#include "NTSC.h"
@@ -112,6 +113,8 @@ int g_nMemoryClearType = MIP_FF_FF_00_00; // Note: -1 = random MIP in Memory.c
CardManager g_CardMgr;
IPropertySheet& sg_PropertySheet = * new CPropertySheet;
+SynchronousEventManager g_SynchronousEventMgr;
+
HANDLE g_hCustomRomF8 = INVALID_HANDLE_VALUE; // Cmd-line specified custom F8 ROM at $F800..$FFFF
static bool g_bCustomRomF8Failed = false; // Set if custom F8 ROM file failed
HANDLE g_hCustomRom = INVALID_HANDLE_VALUE; // Cmd-line specified custom ROM at $C000..$FFFF(16KiB) or $D000..$FFFF(12KiB)
@@ -1424,6 +1427,9 @@ int APIENTRY WinMain(HINSTANCE passinstance, HINSTANCE, LPSTR lpCmdLine, int)
DSUninit();
LogFileOutput("Main: DSUninit()\n");
+ if (g_bRestart)
+ g_SynchronousEventMgr.Reset();
+
if (g_bHookSystemKey)
{
UninitHookThread();
@@ -2214,8 +2220,6 @@ static void RepeatInitialization(void)
g_cmdLine.bBoot = false;
}
}
-
- SetMouseCardInstalled( g_CardMgr.IsMouseCardInstalled() );
}
static void Shutdown(void)
diff --git a/source/Applewin.h b/source/Applewin.h
index 49c446f1..34c8f1d3 100644
--- a/source/Applewin.h
+++ b/source/Applewin.h
@@ -53,6 +53,7 @@ extern bool g_bDisableDirectSoundMockingboard; // Cmd line switch: don't i
extern int g_nMemoryClearType; // Cmd line switch: use specific MIP (Memory Initialization Pattern)
extern class CardManager g_CardMgr;
+extern class SynchronousEventManager g_SynchronousEventMgr;
extern HANDLE g_hCustomRomF8; // INVALID_HANDLE_VALUE if no custom F8 rom
extern HANDLE g_hCustomRom; // INVALID_HANDLE_VALUE if no custom rom
diff --git a/source/CPU.cpp b/source/CPU.cpp
index a7361f7d..957b4fea 100644
--- a/source/CPU.cpp
+++ b/source/CPU.cpp
@@ -96,6 +96,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#ifdef USE_SPEECH_API
#include "Speech.h"
#endif
+#include "SynchronousEventManager.h"
#include "Video.h"
#include "NTSC.h"
#include "Log.h"
@@ -149,8 +150,6 @@ static volatile BOOL g_bNmiFlank = FALSE; // Positive going flank on NMI line
static bool g_irqDefer1Opcode = false;
-static bool g_isMouseCardInstalled = false;
-
//
static eCpuType g_MainCPU = CPU_65C02;
@@ -206,11 +205,6 @@ void ResetCyclesExecutedForDebugger(void)
g_nCyclesExecuted = 0;
}
-void SetMouseCardInstalled(bool installed)
-{
- g_isMouseCardInstalled = installed;
-}
-
//
#include "CPU/cpu_general.inl"
@@ -403,8 +397,13 @@ static __forceinline void NMI(ULONG& uExecutedCycles, BOOL& flagc, BOOL& flagn,
#endif
}
-// NB. No need to save to save-state, as IRQ() follows CheckInterruptSources(), and IRQ() always sets it to false.
-static bool g_irqOnLastOpcodeCycle = false;
+static __forceinline void CheckSynchronousInterruptSources(UINT cycles, ULONG uExecutedCycles)
+{
+ g_SynchronousEventMgr.Update(cycles, uExecutedCycles);
+}
+
+// NB. No need to save to save-state, as IRQ() follows CheckSynchronousInterruptSources(), and IRQ() always sets it to false.
+bool g_irqOnLastOpcodeCycle = false;
static __forceinline void IRQ(ULONG& uExecutedCycles, BOOL& flagc, BOOL& flagn, BOOL& flagv, BOOL& flagz)
{
@@ -435,39 +434,12 @@ static __forceinline void IRQ(ULONG& uExecutedCycles, BOOL& flagc, BOOL& flagn,
#if defined(_DEBUG) && LOG_IRQ_TAKEN_AND_RTI
LogOutput("IRQ\n");
#endif
+ CheckSynchronousInterruptSources(7, uExecutedCycles);
}
g_irqOnLastOpcodeCycle = false;
}
-const int IRQ_CHECK_OPCODE_FULL_SPEED = 40; // ~128 cycles (assume 3 cycles per opcode)
-static int g_fullSpeedOpcodeCount = IRQ_CHECK_OPCODE_FULL_SPEED;
-
-static __forceinline void CheckInterruptSources(ULONG uExecutedCycles, const bool bVideoUpdate)
-{
- if (!bVideoUpdate)
- {
- g_fullSpeedOpcodeCount--;
- if (g_fullSpeedOpcodeCount >= 0)
- return;
- g_fullSpeedOpcodeCount = IRQ_CHECK_OPCODE_FULL_SPEED;
- }
-
- if (MB_UpdateCycles(uExecutedCycles))
- g_irqOnLastOpcodeCycle = true;
-
- if (g_isMouseCardInstalled)
- g_CardMgr.GetMouseCard()->SetVBlank( !VideoGetVblBar(uExecutedCycles) );
-}
-
-// GH#608: IRQ needs to occur within 17 cycles (6 opcodes) of configuring the timer interrupt
-void CpuAdjustIrqCheck(UINT uCyclesUntilInterrupt)
-{
- const UINT opcodesUntilInterrupt = uCyclesUntilInterrupt/3; // assume 3 cycles per opcode
- if (g_bFullSpeed && opcodesUntilInterrupt < IRQ_CHECK_OPCODE_FULL_SPEED)
- g_fullSpeedOpcodeCount = opcodesUntilInterrupt;
-}
-
//===========================================================================
#define READ _READ
@@ -506,7 +478,7 @@ void CpuAdjustIrqCheck(UINT uCyclesUntilInterrupt)
static DWORD InternalCpuExecute(const DWORD uTotalCycles, const bool bVideoUpdate)
{
- if (g_nAppMode == MODE_RUNNING)
+ if (g_nAppMode == MODE_RUNNING || g_nAppMode == MODE_BENCHMARK)
{
if (GetMainCpu() == CPU_6502)
return Cpu6502(uTotalCycles, bVideoUpdate); // Apple ][, ][+, //e, Clones
diff --git a/source/CPU.h b/source/CPU.h
index 70056c6c..c8bab5c5 100644
--- a/source/CPU.h
+++ b/source/CPU.h
@@ -14,7 +14,6 @@ struct regsrec
extern regsrec regs;
extern unsigned __int64 g_nCumulativeCycles;
-void CpuAdjustIrqCheck(UINT uCyclesUntilInterrupt);
void CpuDestroy ();
void CpuCalcCycles(ULONG nExecutedCycles);
DWORD CpuExecute(const DWORD uCycles, const bool bVideoUpdate);
@@ -45,4 +44,3 @@ void SetActiveCpu(eCpuType cpu);
bool Is6502InterruptEnabled(void);
void ResetCyclesExecutedForDebugger(void);
-void SetMouseCardInstalled(bool installed);
diff --git a/source/CPU/cpu6502.h b/source/CPU/cpu6502.h
index 4f9d381e..d7d036ff 100644
--- a/source/CPU/cpu6502.h
+++ b/source/CPU/cpu6502.h
@@ -317,7 +317,7 @@ static DWORD Cpu6502(DWORD uTotalCycles, const bool bVideoUpdate)
}
}
- CheckInterruptSources(uExecutedCycles, bVideoUpdate);
+ CheckSynchronousInterruptSources(uExecutedCycles - uPreviousCycles, uExecutedCycles);
NMI(uExecutedCycles, flagc, flagn, flagv, flagz);
IRQ(uExecutedCycles, flagc, flagn, flagv, flagz);
diff --git a/source/CPU/cpu65C02.h b/source/CPU/cpu65C02.h
index 5d21d58b..425a0b6c 100644
--- a/source/CPU/cpu65C02.h
+++ b/source/CPU/cpu65C02.h
@@ -317,7 +317,7 @@ static DWORD Cpu65C02(DWORD uTotalCycles, const bool bVideoUpdate)
}
}
- CheckInterruptSources(uExecutedCycles, bVideoUpdate);
+ CheckSynchronousInterruptSources(uExecutedCycles - uPreviousCycles, uExecutedCycles);
NMI(uExecutedCycles, flagc, flagn, flagv, flagz);
IRQ(uExecutedCycles, flagc, flagn, flagv, flagz);
diff --git a/source/Common.h b/source/Common.h
index 4f1e0ea5..89daf20f 100644
--- a/source/Common.h
+++ b/source/Common.h
@@ -24,6 +24,7 @@ enum AppMode_e
, MODE_RUNNING // 6502 is running at normal/full speed (Debugger breakpoints may or may not be active)
, MODE_DEBUG // 6502 is paused
, MODE_STEPPING // 6502 is running at normal/full speed (Debugger breakpoints always active)
+ , MODE_BENCHMARK
};
#define SPEED_MIN 0
diff --git a/source/Frame.cpp b/source/Frame.cpp
index 59b8c76e..95abc0e7 100644
--- a/source/Frame.cpp
+++ b/source/Frame.cpp
@@ -1911,10 +1911,11 @@ LRESULT CALLBACK FrameWndProc (
case WM_USER_BENCHMARK: {
UpdateWindow(window);
ResetMachineState();
- g_nAppMode = MODE_LOGO;
DrawStatusArea((HDC)0,DRAW_TITLE);
HCURSOR oldcursor = SetCursor(LoadCursor(0,IDC_WAIT));
+ g_nAppMode = MODE_BENCHMARK;
VideoBenchmark();
+ g_nAppMode = MODE_LOGO;
ResetMachineState();
SetCursor(oldcursor);
break;
diff --git a/source/Mockingboard.cpp b/source/Mockingboard.cpp
index 38d7780b..e405a2d5 100644
--- a/source/Mockingboard.cpp
+++ b/source/Mockingboard.cpp
@@ -86,6 +86,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#include "Memory.h"
#include "Mockingboard.h"
#include "SoundCore.h"
+#include "SynchronousEventManager.h"
#include "YamlHelper.h"
#include "Riff.h"
@@ -115,10 +116,10 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#define SY6522B_Offset 0x80
#define SSI263_Offset 0x40
-#define Phasor_SY6522A_CS 4
-#define Phasor_SY6522B_CS 7
-#define Phasor_SY6522A_Offset (1<m_active)
+ g_SynchronousEventMgr.Remove(id);
+
+ pSyncEvent->SetCycles(timerLatch + kExtraTimerCycles + opcodeCycleAdjust);
+ g_SynchronousEventMgr.Insert(pSyncEvent);
+
+ // It doesn't matter if this overflows (ie. >0xFFFF), since on completion of current opcode it'll be corrected
+ return (USHORT) (timerLatch + opcodeCycleAdjust);
+}
+
static void UpdateIFR(SY6522_AY8910* pMB, BYTE clr_ifr, BYTE set_ifr=0)
{
// Need critical section to avoid data-race: main thread & SSI263Thread can both access IFR
@@ -425,34 +521,29 @@ static void SY6522_Write(BYTE nDevice, BYTE nReg, BYTE nValue)
pMB->sy6522.TIMER1_LATCH.l = nValue;
break;
case 0x05: // TIMER1H_COUNTER
- /* Initiates timer1 & clears time-out of timer1 */
-
- // Clear Timer Interrupt Flag.
- UpdateIFR(pMB, IxR_TIMER1);
-
- pMB->sy6522.TIMER1_LATCH.h = nValue;
- pMB->bLoadT1C = true;
-
- StartTimer1(pMB);
- CpuAdjustIrqCheck(pMB->sy6522.TIMER1_LATCH.w); // Sync IRQ check timeout with 6522 counter underflow - GH#608
+ {
+ UpdateIFR(pMB, IxR_TIMER1); // Clear Timer1 Interrupt Flag
+ pMB->sy6522.TIMER1_LATCH.h = nValue;
+ const UINT id = nDevice*kNumTimersPer6522+0; // TIMER1
+ pMB->sy6522.TIMER1_COUNTER.w = SetTimerSyncEvent(id, nReg, pMB->sy6522.TIMER1_LATCH.w);
+ StartTimer1(pMB);
+ }
break;
case 0x07: // TIMER1H_LATCH
- // Clear Timer1 Interrupt Flag.
- UpdateIFR(pMB, IxR_TIMER1);
+ UpdateIFR(pMB, IxR_TIMER1); // Clear Timer1 Interrupt Flag
pMB->sy6522.TIMER1_LATCH.h = nValue;
break;
case 0x08: // TIMER2L
pMB->sy6522.TIMER2_LATCH.l = nValue;
break;
case 0x09: // TIMER2H
- // Clear Timer2 Interrupt Flag.
- UpdateIFR(pMB, IxR_TIMER2);
-
- pMB->sy6522.TIMER2_LATCH.h = nValue; // NB. Real 6522 doesn't have TIMER2_LATCH.h
- pMB->sy6522.TIMER2_COUNTER.w = pMB->sy6522.TIMER2_LATCH.w;
-
- StartTimer2(pMB);
- CpuAdjustIrqCheck(pMB->sy6522.TIMER2_LATCH.w); // Sync IRQ check timeout with 6522 counter underflow - GH#608
+ {
+ UpdateIFR(pMB, IxR_TIMER2); // Clear Timer2 Interrupt Flag
+ pMB->sy6522.TIMER2_LATCH.h = nValue; // NB. Real 6522 doesn't have TIMER2_LATCH.h
+ const UINT id = nDevice*kNumTimersPer6522+1; // TIMER2
+ pMB->sy6522.TIMER2_COUNTER.w = SetTimerSyncEvent(id, nReg, pMB->sy6522.TIMER2_LATCH.w);
+ StartTimer2(pMB);
+ }
break;
case 0x0a: // SERIAL_SHIFT
break;
@@ -1509,6 +1600,11 @@ void MB_Initialize()
InitializeCriticalSection(&g_CriticalSection);
g_bCritSectionValid = true;
+
+ for (int id=0; idm_active)
+ g_SynchronousEventMgr.Remove(id);
+ }
+
// Not these, as they don't change on a CTRL+RESET or power-cycle:
// g_bMBAvailable = false;
// g_SoundcardType = CT_Empty; // Don't uncomment, else _ASSERT will fire in MB_Read() after an F2->MB_Reset()
@@ -1601,8 +1709,7 @@ void MB_Reset() // CTRL+RESET or power-cycle
static BYTE __stdcall MB_Read(WORD PC, WORD nAddr, BYTE bWrite, BYTE nValue, ULONG nExecutedCycles)
{
- if (g_bFullSpeed)
- MB_UpdateCycles(nExecutedCycles);
+ MB_UpdateCycles(nExecutedCycles);
#ifdef _DEBUG
if(!IS_APPLE2 && MemCheckINTCXROM())
@@ -1665,8 +1772,7 @@ static BYTE __stdcall MB_Read(WORD PC, WORD nAddr, BYTE bWrite, BYTE nValue, ULO
static BYTE __stdcall MB_Write(WORD PC, WORD nAddr, BYTE bWrite, BYTE nValue, ULONG nExecutedCycles)
{
- if (g_bFullSpeed)
- MB_UpdateCycles(nExecutedCycles);
+ MB_UpdateCycles(nExecutedCycles);
#ifdef _DEBUG
if(!IS_APPLE2 && MemCheckINTCXROM())
@@ -1928,7 +2034,7 @@ void MB_PeriodicUpdate(UINT executedCycles)
//-----------------------------------------------------------------------------
-static bool CheckTimerUnderflowAndIrq(USHORT& timerCounter, int& timerIrqDelay, const USHORT nClocks)
+static bool CheckTimerUnderflow(USHORT& timerCounter, int& timerIrqDelay, const USHORT nClocks)
{
if (nClocks == 0)
return false;
@@ -1960,96 +2066,83 @@ static bool CheckTimerUnderflowAndIrq(USHORT& timerCounter, int& timerIrqDelay,
}
// Called by:
-// . CpuExecute() every ~1000 @ 1MHz
-// . CheckInterruptSources() every opcode (or every 40 opcodes at full-speed)
-// . MB_Read() / MB_Write() (only for full-speed)
-bool MB_UpdateCycles(ULONG uExecutedCycles)
+// . CpuExecute() every ~1000 cycles @ 1MHz
+// . MB_SyncEventCallback() on a TIMER1/2 underflow
+// . MB_Read() / MB_Write() (for both normal & full-speed)
+void MB_UpdateCycles(ULONG uExecutedCycles)
{
if (g_SoundcardType == CT_Empty)
- return false;
+ return;
CpuCalcCycles(uExecutedCycles);
UINT64 uCycles = g_nCumulativeCycles - g_uLastCumulativeCycles;
+ _ASSERT(uCycles >= 0);
if (uCycles == 0)
- return false; // Likely when called from CpuExecute()
-
- const bool isOpcode = (uCycles <= 7); // todo: better to pass in a flag?
+ return;
g_uLastCumulativeCycles = g_nCumulativeCycles;
- _ASSERT(uCycles < 0x10000);
- USHORT nClocks = (USHORT) uCycles;
+ _ASSERT(uCycles < 0x10000 || g_nAppMode == MODE_BENCHMARK);
+ USHORT nClocks = (USHORT)uCycles;
- bool bIrqOnLastOpcodeCycle = false;
-
- for (int i=0; isy6522.TIMER1_COUNTER.w, pMB->sy6522.timer1IrqDelay, nClocks);
+ const bool bTimer2Underflow = CheckTimerUnderflow(pMB->sy6522.TIMER2_COUNTER.w, pMB->sy6522.timer2IrqDelay, nClocks);
- if (isOpcode)
+ if (pMB->bTimer1Active && bTimer1Underflow)
{
- bTimer1Irq = CheckTimerUnderflowAndIrq(pMB->sy6522.TIMER1_COUNTER.w, pMB->sy6522.timer1IrqDelay, nClocks-1);
- bTimer1IrqOnLastCycle = CheckTimerUnderflowAndIrq(pMB->sy6522.TIMER1_COUNTER.w, pMB->sy6522.timer1IrqDelay, 1);
- bTimer1Irq = bTimer1Irq || bTimer1IrqOnLastCycle;
- }
- else
- {
- bTimer1Irq = CheckTimerUnderflowAndIrq(pMB->sy6522.TIMER1_COUNTER.w, pMB->sy6522.timer1IrqDelay, nClocks);
- }
-
- const bool bTimer2Irq = CheckTimerUnderflowAndIrq(pMB->sy6522.TIMER2_COUNTER.w, pMB->sy6522.timer2IrqDelay, nClocks);
-
- if (pMB->bTimer1Active && bTimer1Irq)
- {
- UpdateIFR(pMB, 0, IxR_TIMER1);
- bIrqOnLastOpcodeCycle = bTimer1IrqOnLastCycle;
-
- MB_Update();
-
- if ((pMB->sy6522.ACR & RUNMODE) == RM_ONESHOT)
+ pMB->sy6522.TIMER1_COUNTER.w += pMB->sy6522.TIMER1_LATCH.w; // GH#651: account for underflowed cycles too
+ pMB->sy6522.TIMER1_COUNTER.w += kExtraTimerCycles; // GH#652: account for extra 2 cycles
+ // EG. T1C=0xFFFE, T1L=0x0001
+ // . T1C += T1L = 0xFFFF
+ // . T1C += 2 = 0x0001
+ if (pMB->sy6522.TIMER1_COUNTER.w > pMB->sy6522.TIMER1_LATCH.w)
{
- // One-shot mode
- // - Phasor's playback code uses one-shot mode
- StopTimer1(pMB);
+ if (pMB->sy6522.TIMER1_LATCH.w)
+ pMB->sy6522.TIMER1_COUNTER.w %= pMB->sy6522.TIMER1_LATCH.w; // Only occurs if LATCH.w<0x0007 (# cycles for longest opcode)
+ else
+ pMB->sy6522.TIMER1_COUNTER.w = 0;
}
- else
- {
- // Free-running mode
- // - Ultima4/5 change ACCESS_TIMER1 after a couple of IRQs into tune
- pMB->sy6522.TIMER1_COUNTER.w += pMB->sy6522.TIMER1_LATCH.w; // GH#651: account for underflowed cycles too
- pMB->sy6522.TIMER1_COUNTER.w += 2; // GH#652: account for extra 2 cycles (Rockwell, Fig.16: period=N+2cycles)
- // EG. T1C=0xFFFE, T1L=0x0001
- // . T1C += T1L = 0xFFFF
- // . T1C += 2 = 0x0001
- if (pMB->sy6522.TIMER1_COUNTER.w > pMB->sy6522.TIMER1_LATCH.w)
- {
- if (pMB->sy6522.TIMER1_LATCH.w)
- pMB->sy6522.TIMER1_COUNTER.w %= pMB->sy6522.TIMER1_LATCH.w; // Only occurs if LATCH.w<0x0007 (# cycles for longest opcode)
- else
- pMB->sy6522.TIMER1_COUNTER.w = 0;
- }
- StartTimer1(pMB);
- }
- }
-
- if (pMB->bLoadT1C)
- {
- pMB->bLoadT1C = false;
- pMB->sy6522.TIMER1_COUNTER.w = pMB->sy6522.TIMER1_LATCH.w;
- }
-
- if (pMB->bTimer2Active && bTimer2Irq)
- {
- UpdateIFR(pMB, 0, IxR_TIMER2);
-
- // TIMER2 only runs in one-shot mode
- StopTimer2(pMB);
}
}
+}
- return bIrqOnLastOpcodeCycle;
+//-----------------------------------------------------------------------------
+
+static int MB_SyncEventCallback(int id, int /*cycles*/, ULONG uExecutedCycles)
+{
+ SY6522_AY8910* pMB = &g_MB[id / kNumTimersPer6522];
+
+ if ((id & 1) == 0)
+ {
+ _ASSERT(pMB->bTimer1Active);
+ MB_Update();
+
+ UpdateIFR(pMB, 0, IxR_TIMER1);
+
+ if ((pMB->sy6522.ACR & RUNMODE) == RM_ONESHOT)
+ {
+ // One-shot mode
+ // - Phasor's playback code uses one-shot mode
+ StopTimer1(pMB);
+ return 0; // Don't repeat event
+ }
+
+ MB_UpdateCycles(uExecutedCycles);
+
+ StartTimer1(pMB);
+ return pMB->sy6522.TIMER1_COUNTER.w + kExtraTimerCycles;
+ }
+ else
+ {
+ _ASSERT(pMB->bTimer2Active);
+ UpdateIFR(pMB, 0, IxR_TIMER2);
+
+ StopTimer2(pMB); // TIMER2 only runs in one-shot mode
+ return 0; // Don't repeat event
+ }
}
//-----------------------------------------------------------------------------
@@ -2350,6 +2443,21 @@ bool MB_LoadSnapshot(YamlLoadHelper& yamlLoadHelper, UINT slot, UINT version)
StartTimer1(pMB); // Attempt to start timer
}
+ if (pMB->bTimer1Active)
+ {
+ const UINT id = nDeviceNum*kNumTimersPer6522+0; // TIMER1
+ SyncEvent* pSyncEvent = g_syncEvent[id];
+ pSyncEvent->SetCycles(pMB->sy6522.TIMER1_COUNTER.w + kExtraTimerCycles); // NB. use COUNTER, not LATCH
+ g_SynchronousEventMgr.Insert(pSyncEvent);
+ }
+ if (pMB->bTimer2Active)
+ {
+ const UINT id = nDeviceNum*kNumTimersPer6522+1; // TIMER2
+ SyncEvent* pSyncEvent = g_syncEvent[id];
+ pSyncEvent->SetCycles(pMB->sy6522.TIMER2_COUNTER.w + kExtraTimerCycles); // NB. use COUNTER, not LATCH
+ g_SynchronousEventMgr.Insert(pSyncEvent);
+ }
+
// FIXME: currently only support a single speech chip
// NB. g_bVotraxPhoneme is never true, as the phoneme playback completes in SSI263Thread() before this point in the save-state.
// NB. SpeechChip.DurationPhoneme will mostly be non-zero during speech playback, as this is the SSI263 register, not whether the phonene is active.
@@ -2487,6 +2595,21 @@ bool Phasor_LoadSnapshot(YamlLoadHelper& yamlLoadHelper, UINT slot, UINT version
StartTimer1(pMB); // Attempt to start timer
}
+ if (pMB->bTimer1Active)
+ {
+ const UINT id = nDeviceNum*kNumTimersPer6522+0; // TIMER1
+ SyncEvent* pSyncEvent = g_syncEvent[id];
+ pSyncEvent->SetCycles(pMB->sy6522.TIMER1_COUNTER.w + kExtraTimerCycles); // NB. use COUNTER, not LATCH
+ g_SynchronousEventMgr.Insert(pSyncEvent);
+ }
+ if (pMB->bTimer2Active)
+ {
+ const UINT id = nDeviceNum*kNumTimersPer6522+1; // TIMER2
+ SyncEvent* pSyncEvent = g_syncEvent[id];
+ pSyncEvent->SetCycles(pMB->sy6522.TIMER2_COUNTER.w + kExtraTimerCycles); // NB. use COUNTER, not LATCH
+ g_SynchronousEventMgr.Insert(pSyncEvent);
+ }
+
// FIXME: currently only support a single speech chip
if (pMB->SpeechChip.DurationPhoneme || g_bVotraxPhoneme)
{
diff --git a/source/Mockingboard.h b/source/Mockingboard.h
index 49859f58..66d2ebba 100644
--- a/source/Mockingboard.h
+++ b/source/Mockingboard.h
@@ -16,7 +16,7 @@ void MB_CheckCumulativeCycles(); // DEBUG
void MB_SetCumulativeCycles();
void MB_PeriodicUpdate(UINT executedCycles);
void MB_CheckIRQ();
-bool MB_UpdateCycles(ULONG uExecutedCycles);
+void MB_UpdateCycles(ULONG uExecutedCycles);
SS_CARDTYPE MB_GetSoundcardType();
bool MB_IsActive();
DWORD MB_GetVolume();
diff --git a/source/MouseInterface.cpp b/source/MouseInterface.cpp
index 3f15dda2..ab7faf16 100644
--- a/source/MouseInterface.cpp
+++ b/source/MouseInterface.cpp
@@ -44,11 +44,14 @@ Etc.
#include "SaveState_Structs_common.h"
#include "Common.h"
+#include "AppleWin.h" // g_SynchronousEventMgr
+#include "CardManager.h"
#include "CPU.h"
#include "Frame.h" // FrameSetCursorPosByMousePos()
#include "Log.h"
#include "Memory.h"
#include "MouseInterface.h"
+#include "NTSC.h" // NTSC_GetCyclesUntilVBlank()
#include "YamlHelper.h"
#include "../resource/resource.h"
@@ -132,7 +135,8 @@ void M6821_Listener_A( void* objTo, BYTE byData )
CMouseInterface::CMouseInterface(UINT slot) :
Card(CT_MouseInterface),
m_uSlot(slot),
- m_pSlotRom(NULL)
+ m_pSlotRom(NULL),
+ m_syncEvent(slot, 0, SyncEventCallback) // use slot# as "unique" id for MouseInterfaces
{
m_6821.SetListenerB( this, M6821_Listener_B );
m_6821.SetListenerA( this, M6821_Listener_A );
@@ -145,6 +149,9 @@ CMouseInterface::CMouseInterface(UINT slot) :
CMouseInterface::~CMouseInterface()
{
delete [] m_pSlotRom;
+
+ if (m_syncEvent.m_active)
+ g_SynchronousEventMgr.Remove(m_syncEvent.m_id);
}
//===========================================================================
@@ -184,6 +191,10 @@ void CMouseInterface::Initialize(LPBYTE pCxRomPeripheral, UINT uSlot)
_ASSERT(m_uSlot == uSlot);
SetSlotRom(); // Pre: m_bActive == true
RegisterIoHandler(uSlot, &CMouseInterface::IORead, &CMouseInterface::IOWrite, NULL, NULL, this, NULL);
+
+ if (m_syncEvent.m_active) g_SynchronousEventMgr.Remove(m_syncEvent.m_id);
+ m_syncEvent.m_cyclesRemaining = NTSC_GetCyclesUntilVBlank(0);
+ g_SynchronousEventMgr.Insert(&m_syncEvent);
}
#if 0
@@ -221,6 +232,8 @@ void CMouseInterface::Reset()
Clear();
memset( m_byBuff, 0, sizeof( m_byBuff ) );
SetSlotRom();
+
+ // NB. Leave the syncEvent in the list - otherwise nothing else will re-add it!
}
void CMouseInterface::SetSlotRom()
@@ -475,16 +488,10 @@ void CMouseInterface::OnMouseEvent(bool bEventVBL)
}
}
-void CMouseInterface::SetVBlank(bool bVBL)
+int CMouseInterface::SyncEventCallback(int id, int cycles, ULONG /*uExecutedCycles*/)
{
-// _ASSERT(m_bActive); // Only called from CheckInterruptSources()
-
- if ( m_bVBL != bVBL )
- {
- m_bVBL = bVBL;
- if ( m_bVBL ) // Rising edge
- OnMouseEvent(true);
- }
+ g_CardMgr.GetMouseCard()->OnMouseEvent(true);
+ return NTSC_GetCyclesUntilVBlank(cycles);
}
void CMouseInterface::Clear()
diff --git a/source/MouseInterface.h b/source/MouseInterface.h
index ee16975e..1b97aaf3 100644
--- a/source/MouseInterface.h
+++ b/source/MouseInterface.h
@@ -1,6 +1,7 @@
#include "6821.h"
#include "Common.h"
#include "Card.h"
+#include "SynchronousEventManager.h"
class CMouseInterface : public Card
{
@@ -23,7 +24,6 @@ public:
bool IsEnabled() { return m_bEnabled; } // NB. m_bEnabled == true implies that m_bActive == true
bool IsActiveAndEnabled() { return /*IsActive() &&*/ IsEnabled(); } // todo: just use IsEnabled()
void SetEnabled(bool bEnabled) { m_bEnabled = bEnabled; }
- void SetVBlank(bool bVBL);
void GetXY(int& iX, int& iMinX, int& iMaxX, int& iY, int& iMinY, int& iMaxY)
{
iX = m_iX;
@@ -62,6 +62,8 @@ protected:
void SetClampX(int iMinX, int iMaxX);
void SetClampY(int iMinY, int iMaxY);
+ static int SyncEventCallback(int id, int cycles, ULONG uExecutedCycles);
+
void SaveSnapshotMC6821(class YamlSaveHelper& yamlSaveHelper, std::string key);
void LoadSnapshotMC6821(class YamlLoadHelper& yamlLoadHelper, std::string key);
@@ -102,6 +104,8 @@ protected:
bool m_bEnabled; // Windows' mouse events get passed to Apple]['s mouse h/w (m_bEnabled == true implies that m_bActive == true)
LPBYTE m_pSlotRom;
UINT m_uSlot;
+
+ SyncEvent m_syncEvent;
};
namespace DIMouse
diff --git a/source/NTSC.cpp b/source/NTSC.cpp
index c12d3d96..d066bcb6 100644
--- a/source/NTSC.cpp
+++ b/source/NTSC.cpp
@@ -2609,6 +2609,24 @@ UINT NTSC_GetVideoLines(void)
return (GetVideoRefreshRate() == VR_50HZ) ? VIDEO_SCANNER_MAX_VERT_PAL : VIDEO_SCANNER_MAX_VERT;
}
+// Get # cycles until rising Vbl edge: !VBl -> VBl at (0,192)
+// . NB. Called from CMouseInterface::SyncEventCallback(), which occurs *before* NTSC_VideoUpdateCycles()
+// therefore g_nVideoClockVert/Horz will be behind, so correct 'cycleCurrentPos' by adding 'cycles'.
+UINT NTSC_GetCyclesUntilVBlank(int cycles)
+{
+ const UINT cyclesPerFrames = NTSC_GetCyclesPerFrame();
+
+ if (g_bFullSpeed)
+ return cyclesPerFrames; // g_nVideoClockVert/Horz not correct & accuracy isn't important: so just wait a frame's worth of cycles
+
+ const UINT cycleVBl = VIDEO_SCANNER_Y_DISPLAY * VIDEO_SCANNER_MAX_HORZ;
+ const UINT cycleCurrentPos = (g_nVideoClockVert * VIDEO_SCANNER_MAX_HORZ + g_nVideoClockHorz + cycles) % cyclesPerFrames;
+
+ return (cycleCurrentPos < cycleVBl) ?
+ (cycleVBl - cycleCurrentPos) :
+ (cyclesPerFrames - cycleCurrentPos + cycleVBl);
+}
+
bool NTSC_IsVisible(void)
{
return (g_nVideoClockVert < VIDEO_SCANNER_Y_DISPLAY) && (g_nVideoClockHorz >= VIDEO_SCANNER_HORZ_START);
diff --git a/source/NTSC.h b/source/NTSC.h
index e3822541..a5a61440 100644
--- a/source/NTSC.h
+++ b/source/NTSC.h
@@ -23,4 +23,5 @@
UINT NTSC_GetCyclesPerFrame(void);
UINT NTSC_GetCyclesPerLine(void);
UINT NTSC_GetVideoLines(void);
+ UINT NTSC_GetCyclesUntilVBlank(int cycles);
bool NTSC_IsVisible(void);
diff --git a/source/SaveState.cpp b/source/SaveState.cpp
index 87aa3dbc..e2d88170 100644
--- a/source/SaveState.cpp
+++ b/source/SaveState.cpp
@@ -479,7 +479,6 @@ static void Snapshot_LoadState_v2(void)
MemUpdatePaging(TRUE);
- SetMouseCardInstalled( g_CardMgr.IsMouseCardInstalled() );
DebugReset();
if (g_nAppMode == MODE_DEBUG)
DebugDisplay(TRUE);
diff --git a/source/SynchronousEventManager.cpp b/source/SynchronousEventManager.cpp
new file mode 100644
index 00000000..02984738
--- /dev/null
+++ b/source/SynchronousEventManager.cpp
@@ -0,0 +1,168 @@
+/*
+AppleWin : An Apple //e emulator for Windows
+
+Copyright (C) 1994-1996, Michael O'Brien
+Copyright (C) 1999-2001, Oliver Schmidt
+Copyright (C) 2002-2005, Tom Charlesworth
+Copyright (C) 2006-2020, Tom Charlesworth, Michael Pohoreski, Nick Westgate
+
+AppleWin is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+AppleWin is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with AppleWin; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+*/
+
+/* Description: Synchronous Event Manager
+ *
+ * This manager class maintains a linked-list of ordered timer-based event,
+ * where only the head of the list needs updating after every opcode.
+ *
+ * The Nth event in the list will expire in: event[1] + ... + event[N] cycles time.
+ * (So each event has a cycle delta expiry time relative to the previous event.)
+ *
+ * A synchronous event is used for a deterministic event that will occur in N cycles' time,
+ * eg. 6522 timer & Mousecard VBlank. (As opposed to async events, like SSC Rx/Tx interrupts.)
+ *
+ * Events that are active in the list can be removed before they expire,
+ * eg. 6522 timer when the interval changes.
+ *
+ * Author: Various
+ *
+ */
+
+#include "StdAfx.h"
+
+#include "AppleWin.h"
+#include "SynchronousEventManager.h"
+
+void SynchronousEventManager::Insert(SyncEvent* pNewEvent)
+{
+ pNewEvent->m_active = true; // add always succeeds
+
+ if (!m_syncEventHead)
+ {
+ m_syncEventHead = pNewEvent;
+ return;
+ }
+
+ // walk list to find where to insert new event
+
+ SyncEvent* pPrevEvent = NULL;
+ SyncEvent* pCurrEvent = m_syncEventHead;
+ int newEventExtraCycles = pNewEvent->m_cyclesRemaining;
+
+ while (pCurrEvent)
+ {
+ if (newEventExtraCycles >= pCurrEvent->m_cyclesRemaining)
+ {
+ newEventExtraCycles -= pCurrEvent->m_cyclesRemaining;
+
+ pPrevEvent = pCurrEvent;
+ pCurrEvent = pCurrEvent->m_next;
+
+ if (!pCurrEvent) // end of list
+ {
+ pPrevEvent->m_next = pNewEvent;
+ pNewEvent->m_cyclesRemaining = newEventExtraCycles;
+ }
+
+ continue;
+ }
+
+ // insert new event
+ if (!pPrevEvent)
+ m_syncEventHead = pNewEvent;
+ else
+ pPrevEvent->m_next = pNewEvent;
+ pNewEvent->m_next = pCurrEvent;
+
+ pNewEvent->m_cyclesRemaining = newEventExtraCycles;
+
+ // update cycles for next event
+ if (pCurrEvent)
+ {
+ pCurrEvent->m_cyclesRemaining -= newEventExtraCycles;
+ _ASSERT(pCurrEvent->m_cyclesRemaining >= 0);
+ }
+
+ return;
+ }
+}
+
+bool SynchronousEventManager::Remove(int id)
+{
+ SyncEvent* pPrevEvent = NULL;
+ SyncEvent* pCurrEvent = m_syncEventHead;
+
+ while (pCurrEvent)
+ {
+ if (pCurrEvent->m_id != id)
+ {
+ pPrevEvent = pCurrEvent;
+ pCurrEvent = pCurrEvent->m_next;
+ continue;
+ }
+
+ // remove event
+ if (!pPrevEvent)
+ m_syncEventHead = pCurrEvent->m_next;
+ else
+ pPrevEvent->m_next = pCurrEvent->m_next;
+
+ int oldEventExtraCycles = pCurrEvent->m_cyclesRemaining;
+ pPrevEvent = pCurrEvent;
+ pCurrEvent = pCurrEvent->m_next;
+
+ pPrevEvent->m_active = false;
+ pPrevEvent->m_next = NULL;
+
+ // update cycles for next event
+ if (pCurrEvent)
+ pCurrEvent->m_cyclesRemaining += oldEventExtraCycles;
+
+ return true;
+ }
+
+ _ASSERT(0);
+ return false;
+}
+
+extern bool g_irqOnLastOpcodeCycle;
+
+void SynchronousEventManager::Update(int cycles, ULONG uExecutedCycles)
+{
+ SyncEvent* pCurrEvent = m_syncEventHead;
+
+ if (!pCurrEvent)
+ return;
+
+ pCurrEvent->m_cyclesRemaining -= cycles;
+ if (pCurrEvent->m_cyclesRemaining <= 0)
+ {
+ if (pCurrEvent->m_cyclesRemaining == 0)
+ g_irqOnLastOpcodeCycle = true; // IRQ occurs on last cycle of opcode
+
+ int cyclesUnderflowed = -pCurrEvent->m_cyclesRemaining;
+
+ pCurrEvent->m_cyclesRemaining = pCurrEvent->m_callback(pCurrEvent->m_id, cycles, uExecutedCycles);
+ m_syncEventHead = pCurrEvent->m_next; // unlink this event
+
+ pCurrEvent->m_active = false;
+ pCurrEvent->m_next = NULL;
+
+ // Always Update even if cyclesUnderflowed=0, as next event may have cycleRemaining=0 (ie. the 2 events fire at the same time)
+ Update(cyclesUnderflowed, uExecutedCycles); // update (potential) next event with underflow cycles
+
+ if (pCurrEvent->m_cyclesRemaining)
+ Insert(pCurrEvent); // re-add event
+ }
+}
diff --git a/source/SynchronousEventManager.h b/source/SynchronousEventManager.h
new file mode 100644
index 00000000..d0b9f621
--- /dev/null
+++ b/source/SynchronousEventManager.h
@@ -0,0 +1,50 @@
+#pragma once
+
+class SyncEvent;
+
+class SynchronousEventManager
+{
+public:
+ SynchronousEventManager() : m_syncEventHead(NULL)
+ {}
+ ~SynchronousEventManager(){}
+
+ SyncEvent* GetHead(void) { return m_syncEventHead; }
+ void SetHead(SyncEvent* head) { m_syncEventHead = head; }
+
+ void Insert(SyncEvent* pNewEvent);
+ bool Remove(int id);
+ void Update(int cycles, ULONG uExecutedCycles);
+ void Reset(void) { m_syncEventHead = NULL; }
+
+private:
+ SyncEvent* m_syncEventHead;
+};
+
+//
+
+typedef int (*syncEventCB)(int id, int cycles, ULONG uExecutedCycles);
+
+class SyncEvent
+{
+public:
+ SyncEvent(int id, int initCycles, syncEventCB callback)
+ : m_id(id),
+ m_cyclesRemaining(initCycles),
+ m_active(false),
+ m_callback(callback),
+ m_next(NULL)
+ {}
+ ~SyncEvent(){}
+
+ void SetCycles(int cycles)
+ {
+ m_cyclesRemaining = cycles;
+ }
+
+ int m_id;
+ int m_cyclesRemaining;
+ bool m_active;
+ syncEventCB m_callback;
+ SyncEvent* m_next;
+};
diff --git a/source/Video.cpp b/source/Video.cpp
index 33fe6a73..13036808 100644
--- a/source/Video.cpp
+++ b/source/Video.cpp
@@ -172,6 +172,7 @@ void VideoInitialize ()
//===========================================================================
void VideoBenchmark () {
+ _ASSERT(g_nAppMode == MODE_BENCHMARK);
Sleep(500);
// PREPARE TWO DIFFERENT FRAME BUFFERS, EACH OF WHICH HAVE HALF OF THE
diff --git a/test/TestCPU6502/TestCPU6502-vs2008.vcproj b/test/TestCPU6502/TestCPU6502-vs2008.vcproj
index f5582875..774b1580 100644
--- a/test/TestCPU6502/TestCPU6502-vs2008.vcproj
+++ b/test/TestCPU6502/TestCPU6502-vs2008.vcproj
@@ -194,6 +194,10 @@
RelativePath=".\stdafx.h"
>
+
+
diff --git a/test/TestCPU6502/TestCPU6502-vs2019.vcxproj b/test/TestCPU6502/TestCPU6502-vs2019.vcxproj
index 4166d508..0454719d 100644
--- a/test/TestCPU6502/TestCPU6502-vs2019.vcxproj
+++ b/test/TestCPU6502/TestCPU6502-vs2019.vcxproj
@@ -11,6 +11,7 @@
+
diff --git a/test/TestCPU6502/TestCPU6502-vs2019.vcxproj.filters b/test/TestCPU6502/TestCPU6502-vs2019.vcxproj.filters
index 81896f42..cf1e4e54 100644
--- a/test/TestCPU6502/TestCPU6502-vs2019.vcxproj.filters
+++ b/test/TestCPU6502/TestCPU6502-vs2019.vcxproj.filters
@@ -13,6 +13,9 @@
Source Files
+
+ Source Files
+
diff --git a/test/TestCPU6502/TestCPU6502.cpp b/test/TestCPU6502/TestCPU6502.cpp
index 62634e16..3a248699 100644
--- a/test/TestCPU6502/TestCPU6502.cpp
+++ b/test/TestCPU6502/TestCPU6502.cpp
@@ -3,10 +3,12 @@
#include "../../source/Applewin.h"
#include "../../source/CPU.h"
#include "../../source/Memory.h"
+#include "../../source/SynchronousEventManager.h"
// From Applewin.cpp
bool g_bFullSpeed = false;
enum AppMode_e g_nAppMode = MODE_RUNNING;
+SynchronousEventManager g_SynchronousEventMgr;
// From Memory.cpp
LPBYTE memwrite[0x100]; // TODO: Init
@@ -27,8 +29,7 @@ iofunction IOWrite[256] = {0}; // TODO: Init
regsrec regs;
-static const int IRQ_CHECK_TIMEOUT = 128;
-static signed int g_nIrqCheckTimeout = IRQ_CHECK_TIMEOUT;
+bool g_irqOnLastOpcodeCycle = false;
static eCpuType g_ActiveCPU = CPU_65C02;
@@ -54,7 +55,7 @@ static __forceinline void DoIrqProfiling(DWORD uCycles)
{
}
-static __forceinline void CheckInterruptSources(ULONG uExecutedCycles, const bool bVideoUpdate)
+static __forceinline void CheckSynchronousInterruptSources(UINT cycles, ULONG uExecutedCycles)
{
}
@@ -1253,6 +1254,62 @@ int GH321_test()
//-------------------------------------
+int testCB(int id, int cycles, ULONG uExecutedCycles)
+{
+ return 0;
+}
+
+int SyncEvents_test(void)
+{
+ SyncEvent syncEvent0(0, 0x10, testCB);
+ SyncEvent syncEvent1(1, 0x20, testCB);
+ SyncEvent syncEvent2(2, 0x30, testCB);
+ SyncEvent syncEvent3(3, 0x40, testCB);
+
+ g_SynchronousEventMgr.Insert(&syncEvent0);
+ g_SynchronousEventMgr.Insert(&syncEvent1);
+ g_SynchronousEventMgr.Insert(&syncEvent2);
+ g_SynchronousEventMgr.Insert(&syncEvent3);
+ // id0 -> id1 -> id2 -> id3
+ if (syncEvent0.m_cyclesRemaining != 0x10) return 1;
+ if (syncEvent1.m_cyclesRemaining != 0x10) return 1;
+ if (syncEvent2.m_cyclesRemaining != 0x10) return 1;
+ if (syncEvent3.m_cyclesRemaining != 0x10) return 1;
+
+ g_SynchronousEventMgr.Remove(1);
+ g_SynchronousEventMgr.Remove(3);
+ g_SynchronousEventMgr.Remove(0);
+ if (syncEvent2.m_cyclesRemaining != 0x30) return 1;
+ g_SynchronousEventMgr.Remove(2);
+
+ //
+
+ syncEvent0.m_cyclesRemaining = 0x40;
+ syncEvent1.m_cyclesRemaining = 0x30;
+ syncEvent2.m_cyclesRemaining = 0x20;
+ syncEvent3.m_cyclesRemaining = 0x10;
+
+ g_SynchronousEventMgr.Insert(&syncEvent0);
+ g_SynchronousEventMgr.Insert(&syncEvent1);
+ g_SynchronousEventMgr.Insert(&syncEvent2);
+ g_SynchronousEventMgr.Insert(&syncEvent3);
+ // id3 -> id2 -> id1 -> id0
+ if (syncEvent0.m_cyclesRemaining != 0x10) return 1;
+ if (syncEvent1.m_cyclesRemaining != 0x10) return 1;
+ if (syncEvent2.m_cyclesRemaining != 0x10) return 1;
+ if (syncEvent3.m_cyclesRemaining != 0x10) return 1;
+
+ g_SynchronousEventMgr.Remove(3);
+ g_SynchronousEventMgr.Remove(0);
+ g_SynchronousEventMgr.Remove(1);
+ if (syncEvent2.m_cyclesRemaining != 0x20) return 1;
+ g_SynchronousEventMgr.Remove(2);
+
+ return 0;
+}
+
+//-------------------------------------
+
int _tmain(int argc, _TCHAR* argv[])
{
int res = 1;
@@ -1277,5 +1334,8 @@ int _tmain(int argc, _TCHAR* argv[])
res = GH292_test();
if (res) return res;
+ res = SyncEvents_test();
+ if (res) return res;
+
return 0;
}