|
|
/****************************** Module Header ******************************\
* Module Name: timers.c * * Copyright (c) 1985 - 1999, Microsoft Corporation * * This module contains the user timer APIs and support routines. * * History: * 12-Nov-1990 DarrinM Created. * 08-Apr-1992 DarrinM Switched to PM/Win3-like ScanTimers model. \***************************************************************************/
#define _TIMERS 1 // uses a LARGE_INTEGER
#include "precomp.h"
#pragma hdrstop
/*
* Make sure that if we return a timer id that it is a WORD value. This * will ensure that WOW doesn't need to handle-translate return values * from SetTimer(). * * Start with a large number so that FindTimer() doesn't find a timer we * calculated with a low cTimerId if the app happens to pass in NULL pwnd * and a low id (like 1). */ #define TIMERID_MAX 0x7FFF
#define TIMERID_MIN 0x100
#define ELAPSED_MAX 0x7FFFFFFF
#define SYSRIT_TIMER (TMRF_SYSTEM | TMRF_RIT)
WORD cTimerId = TIMERID_MAX;
/***************************************************************************\
* _SetTimer (API) * * This API will start the specified timer. * * History: * 15-Nov-1990 DavidPe Created. \***************************************************************************/ UINT_PTR _SetTimer( PWND pwnd, UINT_PTR nIDEvent, UINT dwElapse, TIMERPROC_PWND pTimerFunc) { /*
* Prevent apps from setting a Timer with a window proc to another app */ if (pwnd && (PpiCurrent() != GETPTI(pwnd)->ppi)) {
RIPERR1(ERROR_ACCESS_DENIED, RIP_WARNING, "Calling SetTimer with window of another process %lX", pwnd);
return 0; }
return InternalSetTimer(pwnd, nIDEvent, dwElapse, pTimerFunc, 0); }
/***************************************************************************\
* _SetSystemTimer * * This API will start start a system timer which will generate WM_SYSTIMER * messages rather than WM_TIMER * * History: * 15-Nov-1990 DavidPe Created. * 21-Jan-1991 IanJa Prefix '_' denotes export function (not API) \***************************************************************************/
UINT_PTR _SetSystemTimer( PWND pwnd, UINT_PTR nIDEvent, DWORD dwElapse, TIMERPROC_PWND pTimerFunc) { /*
* Prevent apps from setting a Timer with a window proc to another app */ if (pwnd && PpiCurrent() != GETPTI(pwnd)->ppi) { RIPERR1(ERROR_ACCESS_DENIED, RIP_WARNING, "Calling SetSystemTimer with window of another process 0x%p", pwnd);
return 0; }
return InternalSetTimer(pwnd, nIDEvent, dwElapse, pTimerFunc, TMRF_SYSTEM); }
/***************************************************************************\
* FreeTimer * * This function does the actual unlinking and freeing of the timer structure. * I pulled it out of FindTimer() so it could be shared with DestroyQueues- * Timers. * Sets *pptmr to point to the next TIMER struct (NULL if none) * * History: * 15-Feb-1991 DarrinM Pulled from FindTimer(). \***************************************************************************/
VOID FreeTimer( PTIMER ptmr) {
CheckCritIn();
/*
* Mark it for destruction. If it the object is locked it can't * be freed right now. */ if (!HMMarkObjectDestroy((PVOID)ptmr)) return;
/*
* If this timer was just about to be processed, decrement * the ready-count since we're blowing it off. */ if (ptmr->flags & TMRF_READY) DecTimerCount(ptmr->pti);
/*
* Unlock the window */ Unlock(&ptmr->spwnd);
/*
* Unlink this timer */ if (ptmr->ptmrPrev) { ptmr->ptmrPrev->ptmrNext = ptmr->ptmrNext; } else { gptmrFirst = ptmr->ptmrNext; }
if (ptmr->ptmrNext) { ptmr->ptmrNext->ptmrPrev = ptmr->ptmrPrev; }
/*
* Free up the TIMER structure. */ HMFreeObject((PVOID)ptmr); }
/***************************************************************************\
* FindTimer * * This function will find a timer that matches the parameters. We also * deal with killing timers here since it's easier to remove items from * the list while we're scanning it. * * History: * 15-Nov-1990 DavidPe Created. \***************************************************************************/
PTIMER FindTimer( PWND pwnd, UINT_PTR nID, UINT flags, BOOL fKill) { PTIMER ptmr;
ptmr = gptmrFirst;
while (ptmr != NULL) {
/*
* Is this the timer we're looking for? */ if ((ptmr->spwnd == pwnd) && (ptmr->nID == nID) && (ptmr->flags & SYSRIT_TIMER) == (flags & SYSRIT_TIMER)) {
/*
* Are we being called from KillTimer()? If so, destroy the * timer. return != 0 because *pptmr is gone. */ if (fKill) { FreeTimer(ptmr); return (PTIMER)TRUE; }
/*
* Found the timer, break out of the loop. */ break; }
/*
* No, try the next one. */ ptmr = ptmr->ptmrNext; }
return ptmr; }
/***************************************************************************\
* InternalSetTimer * * This is the guts of SetTimer that actually gets things going. * * NOTE (darrinm): Technically there is a bit of latency (the time it takes * between SetTimer's NtSetEvent and when the RIT wakes up and calls ScanTimers) * between when SetTimer is called and when the counter starts counting down. * This is uncool but it should be a very short amount of time because the RIT * is high-priority. If it becomes a problem I know how to fix it. * * History: * 15-Nov-1990 DavidPe Created. \***************************************************************************/
UINT_PTR InternalSetTimer( PWND pwnd, UINT_PTR nIDEvent, UINT dwElapse, TIMERPROC_PWND pTimerFunc, UINT flags) { LARGE_INTEGER liT = {1, 0}; PTIMER ptmr; PTHREADINFO ptiCurrent;
CheckCritIn();
/*
* Assert if someone tries to set a timer after cleanup initiated. */ if (gbCleanupInitiated) { RIPMSGF0(RIP_ERROR, "Too late to create a timer."); return 0; }
/*
* We need to make sure dwElapse isn't too big. NtUserSetTimer ensures * that no app passes in zero for the timeout value; let's assert here * that that remains the case, and that no one internally does this. */ UserAssert(dwElapse != 0); if (dwElapse > ELAPSED_MAX) { RIPMSGF1(RIP_WARNING, "Timer period (0x%x) is too big", dwElapse);
dwElapse = ELAPSED_MAX; }
/*
* Attempt to first locate the timer, then create a new one * if one isn't found. */ if ((ptmr = FindTimer(pwnd, nIDEvent, flags, FALSE)) == NULL) {
/*
* Not found. Create a new one. */ ptmr = (PTIMER)HMAllocObject(NULL, NULL, TYPE_TIMER, sizeof(TIMER)); if (ptmr == NULL) { return 0; }
ptmr->spwnd = NULL;
if (pwnd == NULL) { WORD timerIdInitial = cTimerId;
/*
* Pick a unique, unused timer ID. */ do { if (--cTimerId <= TIMERID_MIN) { cTimerId = TIMERID_MAX; }
if (cTimerId == timerIdInitial) { /*
* Flat out of timers bud. */ HMFreeObject(ptmr); return 0; } } while (FindTimer(NULL, cTimerId, flags, FALSE) != NULL);
ptmr->nID = (UINT)cTimerId; } else { ptmr->nID = nIDEvent; }
/*
* Link the new timer into the front of the list. * Handily this works even when gptmrFirst is NULL. */ ptmr->ptmrNext = gptmrFirst; ptmr->ptmrPrev = NULL; if (gptmrFirst) { gptmrFirst->ptmrPrev = ptmr; } gptmrFirst = ptmr; } else { /*
* If this timer was just about to be processed, decrement * cTimersReady since we're resetting it. */ if (ptmr->flags & TMRF_READY) { DecTimerCount(ptmr->pti); } }
/*
* If pwnd is NULL, create a unique id by * using the timer handle. RIT timers are 'owned' by the RIT pti * so they are not deleted when the creating pti dies. * * We used to record the pti as the pti of the window if one was * specified. This is not what Win 3.1 does and it broke 10862 * where some merge app was setting the timer on winword's window * it it still expected to get the messages not winword. * * MS Visual C NT was counting on this bug in the NT 3.1 so if * a thread sets a timer for a window in another thread in the * same process the timer goes off in the thread of the window. * You can see this by doing a build in msvcnt and the files being * compiled do not show up. */ ptiCurrent = (PTHREADINFO)(W32GetCurrentThread()); /*
* This will be NULL * for a non-GUI thread. */
if (pwnd == NULL) {
if (flags & TMRF_RIT) { ptmr->pti = gptiRit; } else { ptmr->pti = ptiCurrent; UserAssert(ptiCurrent); }
} else {
/*
* As enforced in the API wrappers. We shouldn't get here * any other way for an app timer. * * Always use pti of the window when TMRF_PTIWINDOW is passed in. */ if ((ptiCurrent->TIF_flags & TIF_16BIT) && !(flags & TMRF_PTIWINDOW)) { ptmr->pti = ptiCurrent; UserAssert(ptiCurrent); } else { ptmr->pti = GETPTI(pwnd); } }
/*
* Initialize the timer-struct. * * NOTE: The ptiOptCreator is used to identify a JOURNAL-timer. We * want to allow these timers to be destroyed when the creator * thread goes away. For other threads that create timers across * threads, we do not want to destroy these timers when the * creator goes away. Currently, we're only checking for a * TMRF_RIT. However, in the future we might want to add this * same check for TMRF_SYSTEM. */ Lock(&(ptmr->spwnd), pwnd);
ptmr->cmsCountdown = ptmr->cmsRate = dwElapse; ptmr->flags = flags | TMRF_INIT; ptmr->pfn = pTimerFunc; ptmr->ptiOptCreator = (flags & TMRF_RIT ? ptiCurrent : NULL);
/*
* Force the RIT to scan timers. * * N.B. The following code sets the raw input thread timer to expire * at the absolute time 1 which is very far into the past. This * causes the timer to immediately expire before the set timer * call returns. */ if (ptiCurrent == gptiRit) { /*
* Don't let RIT timer loop reset the master timer - we already have. */ gbMasterTimerSet = TRUE; }
UserAssert(gptmrMaster); KeSetTimer(gptmrMaster, liT, NULL);
/*
* Windows 3.1 returns the timer ID if non-zero, otherwise it returns 1. */ return (ptmr->nID == 0 ? 1 : ptmr->nID); }
/***************************************************************************\
* _KillTimer (API) * * This API will stop a timer from sending WM_TIMER messages. * * History: * 15-Nov-1990 DavidPe Created. \***************************************************************************/
BOOL _KillTimer( PWND pwnd, UINT_PTR nIDEvent) { return KillTimer2(pwnd, nIDEvent, FALSE); }
/***************************************************************************\
* _KillSystemTimer * * This API will stop a system timer from sending WM_SYSTIMER messages. * * History: * 15-Nov-1990 DavidPe Created. * 21-Jan-1991 IanJa Prefix '_' denotes export function (not API) \***************************************************************************/
BOOL _KillSystemTimer( PWND pwnd, UINT_PTR nIDEvent) { return KillTimer2(pwnd, nIDEvent, TRUE); }
/***************************************************************************\
* KillTimer2 * * This is the guts of KillTimer that actually kills the timer. * * History: * 15-Nov-1990 DavidPe Created. \***************************************************************************/
BOOL KillTimer2( PWND pwnd, UINT_PTR nIDEvent, BOOL fSystemTimer) { /*
* Call FindTimer() with fKill == TRUE. This will * basically delete the timer. */ return (FindTimer(pwnd, nIDEvent, (fSystemTimer ? TMRF_SYSTEM : 0), TRUE) != NULL); }
/***************************************************************************\
* DestroyQueuesTimers * * This function scans through all the timers and destroys any that are * associated with the specified queue. * * History: * 15-Feb-1991 DarrinM Created. \***************************************************************************/
VOID DestroyThreadsTimers( PTHREADINFO pti) { PTIMER ptmr;
ptmr = gptmrFirst;
while (ptmr != NULL) {
/*
* Is this one of the timers we're looking for? If so, destroy it. */ if (ptmr->pti == pti || ptmr->ptiOptCreator == pti) { PTIMER ptmrNext = ptmr->ptmrNext; FreeTimer(ptmr); ptmr = ptmrNext; } else { ptmr = ptmr->ptmrNext; } } }
/***************************************************************************\
* DestroyWindowsTimers * * This function scans through all the timers and destroys any that are * associated with the specified window. * * History: * 04-Jun-1991 DarrinM Created. \***************************************************************************/
VOID DestroyWindowsTimers( PWND pwnd) { PTIMER ptmr;
ptmr = gptmrFirst;
while (ptmr != NULL) {
/*
* Is this one of the timers we're looking for? If so, destroy it. */ if (ptmr->spwnd == pwnd) { PTIMER ptmrNext = ptmr->ptmrNext; FreeTimer(ptmr); ptmr = ptmrNext; } else { ptmr = ptmr->ptmrNext; } } }
/***************************************************************************\
* DoTimer * * This function gets called from xxxPeekMessage() if the QS_TIMER bit is * set. If this timer is okay with the filter specified the appropriate * WM_*TIMER message will be placed in 'pmsg' and the timer will be reset. * * History: * 15-Nov-1990 DavidPe Created. * 27-NOv-1991 DavidPe Changed to move 'found' timers to end of list. \***************************************************************************/
BOOL DoTimer( PWND pwndFilter) { PTHREADINFO pti; PTIMER ptmr; PTIMER ptmrNext; PQMSG pqmsg;
CheckCritIn();
pti = PtiCurrent();
/*
* Search for a timer that belongs to this queue. */ ptmr = gptmrFirst;
while (ptmr != NULL) {
/*
* Has this timer gone off and is it one we're looking for? */ if ((ptmr->flags & TMRF_READY) && (ptmr->pti == pti) && CheckPwndFilter(ptmr->spwnd, pwndFilter)) {
/*
* We found an appropriate timer. Put it in the app's queue and * return success. */ if ((pqmsg = AllocQEntry(&pti->mlPost)) != NULL) {
/*
* Store the message and set the QS_POSTMESSAGE bit so the * thread knows it has a message. */ StoreQMessage(pqmsg, ptmr->spwnd, (UINT)((ptmr->flags & TMRF_SYSTEM) ? WM_SYSTIMER : WM_TIMER), (WPARAM)ptmr->nID, (LPARAM)ptmr->pfn, 0, 0, 0); #ifdef REDIRECTION
StoreQMessagePti(pqmsg, pti); #endif // REDIRECTION
SetWakeBit(pti, QS_POSTMESSAGE | QS_ALLPOSTMESSAGE); }
/*
* Reset this timer. */ ptmr->flags &= ~TMRF_READY; DecTimerCount(ptmr->pti);
/*
* If there are other timers in the system move this timer * to the end of the list so other timers in for this queue * get a chance to go off. */ ptmrNext = ptmr->ptmrNext; if (ptmrNext != NULL) {
/*
* Remove ptmr from its place in the list. */ if (ptmr->ptmrPrev) { ptmr->ptmrPrev->ptmrNext = ptmr->ptmrNext; } else gptmrFirst = ptmr->ptmrNext;
ptmrNext->ptmrPrev = ptmr->ptmrPrev;
/*
* Move to the last TIMER of the list. */ while (ptmrNext->ptmrNext != NULL) ptmrNext = ptmrNext->ptmrNext;
/*
* Insert this timer at the end. */ ptmrNext->ptmrNext = ptmr; ptmr->ptmrPrev = ptmrNext; ptmr->ptmrNext = NULL; }
return TRUE; }
ptmr = ptmr->ptmrNext; }
return FALSE; }
/***************************************************************************\
* DecTimerCount * * This routine decrements cTimersReady and clears QS_TIMER if the count * goes down to zero. * * History: * 21-Jan-1991 DavidPe Created. \***************************************************************************/
VOID DecTimerCount( PTHREADINFO pti) { CheckCritIn();
if (--pti->cTimersReady == 0) pti->pcti->fsWakeBits &= ~QS_TIMER; }
/***************************************************************************\
* JournalTimer * * * History: * 04-Mar-1991 DavidPe Created. \***************************************************************************/
VOID JournalTimer( PWND pwnd, UINT message, UINT_PTR nID, LPARAM lParam) { PTHREADINFO pti;
DBG_UNREFERENCED_PARAMETER(pwnd); DBG_UNREFERENCED_PARAMETER(message); DBG_UNREFERENCED_PARAMETER(nID);
/*
* We've already entered the critical section. */ if (pti = ((PTIMER)lParam)->ptiOptCreator) WakeSomeone(pti->pq, pti->pq->msgJournal, NULL);
return; }
/***************************************************************************\
* SetJournalTimer * * Sets an NT timer that goes off in 'dt' milliseconds and will wake * up 'pti' at that time. This is used in journal playback code to * simulate the timing in which events were originally given to the system. * * History: * 04-Mar-1991 DavidPe Created. \***************************************************************************/
void SetJournalTimer( DWORD dt, UINT msgJournal) { static UINT_PTR idJournal = 0;
PtiCurrent()->pq->msgJournal = msgJournal;
/*
* Remember idJournal - because TMRF_ONESHOT timers stay in the timer * list - by remembering the idJournal, we always reuse the same timer * rather than creating new ones always. */ idJournal = InternalSetTimer(NULL, idJournal, dt, JournalTimer, TMRF_RIT | TMRF_ONESHOT); }
/***************************************************************************\
* StartTimers * * Prime the timer pump by starting the cursor restoration timer. * * History: * 02-Apr-1992 DarrinM Created. \***************************************************************************/
UINT_PTR StartTimers(VOID) { /*
* Let GDI know that it can start settings timers on the RIT. */ GreStartTimers();
/*
* TMRF_RIT timers are called directly from ScanTimers -- no nasty * thread switching for these boys. */ return InternalSetTimer(NULL, 0, 1000, xxxHungAppDemon, TMRF_RIT); }
/***************************************************************************\
* TimersProc * * Deal with the timers. Called from RawInputThread. * * History: * 11-11-1996 CLupu Created. \***************************************************************************/ VOID TimersProc( VOID) { DWORD dmsSinceLast, cmsCur, dmsNextTimer; LARGE_INTEGER liT; PTIMER ptmr;
EnterCrit();
/*
* Calculate how long it was since the last time we processed timers so * we can subtract that much time from each timer's countdown value. */ cmsCur = NtGetTickCount(); dmsSinceLast = ComputePastTickDelta(cmsCur, gcmsLastTimer); gcmsLastTimer = cmsCur;
/*
* dmsNextTimer is the time delta before the next * timer should go off. As we loop through the * timers below this will shrink to the smallest * cmsCountdown value in the list. */ dmsNextTimer = ELAPSED_MAX; ptmr = gptmrFirst; gbMasterTimerSet = FALSE; while (ptmr != NULL) {
/*
* ONESHOT timers go to a WAITING state after * they go off. This allows us to leave them * in the list but keep them from going off * over and over. */ if (ptmr->flags & TMRF_WAITING) { ptmr = ptmr->ptmrNext; continue; }
/*
* The first time we encounter a timer we don't * want to set it off, we just want to use it to * compute the shortest countdown value. */ if (ptmr->flags & TMRF_INIT) { ptmr->flags &= ~TMRF_INIT;
} else { /*
* If this timer is going off, wake up its * owner. */ if (ptmr->cmsCountdown > dmsSinceLast) { ptmr->cmsCountdown -= dmsSinceLast; } else { UserAssert(ptmr->cmsRate <= ELAPSED_MAX); ptmr->cmsCountdown = ptmr->cmsRate;
/*
* If the timer's owner hasn't handled the * last time it went off yet, throw this event * away. */ if (!(ptmr->flags & TMRF_READY)) { /*
* A ONESHOT timer goes into a WAITING state * until SetTimer is called again to reset it. */ if (ptmr->flags & TMRF_ONESHOT) ptmr->flags |= TMRF_WAITING;
/*
* RIT timers have the distinction of being * called directly and executing serially with * with incoming timer events. * NOTE: RIT timers get called while we're * inside the critical section. */ if (ptmr->flags & TMRF_RIT) { TL tlTimer;
ThreadLock(ptmr, &tlTimer); /*
* May set gbMasterTimerSet */ (ptmr->pfn)(NULL, WM_SYSTIMER, ptmr->nID, (LPARAM)ptmr);
if (HMIsMarkDestroy(ptmr)) { ptmr = ptmr->ptmrNext; ThreadUnlock(&tlTimer); continue; } ThreadUnlock(&tlTimer);
} else { ptmr->flags |= TMRF_READY; ptmr->pti->cTimersReady++; SetWakeBit(ptmr->pti, QS_TIMER); } } } }
/*
* Remember the shortest time left of the timers. */ if (ptmr->cmsCountdown < dmsNextTimer) { dmsNextTimer = ptmr->cmsCountdown; }
/*
* Advance to the next timer structure. */ ptmr = ptmr->ptmrNext; }
if (!gbMasterTimerSet) { /*
* Time in NT should be negative to specify a relative * time. It's also in hundred nanosecond units so multiply * by 10000 to get the right value from milliseconds. */ liT.QuadPart = Int32x32To64(-10000, dmsNextTimer); KeSetTimer(gptmrMaster, liT, NULL); }
LeaveCrit(); }
/***************************************************************************\
* xxxSystemTimerProc() * * 11/15/96 GerardoB Created \***************************************************************************/ VOID xxxSystemTimerProc(PWND pwnd, UINT msg, UINT_PTR id, LPARAM lParam) { CheckLock(pwnd); UNREFERENCED_PARAMETER(msg); UNREFERENCED_PARAMETER(id); UNREFERENCED_PARAMETER(lParam);
switch (id) { case IDSYS_LAYER: { PDCE pdce;
UserAssert(gnVisibleRedirectedCount > 0);
for (pdce = gpDispInfo->pdceFirst; pdce != NULL; pdce = pdce->pdceNext) {
if (pdce->DCX_flags & (DCX_INVALID | DCX_DESTROYTHIS)) continue;
if ((pdce->DCX_flags & DCX_REDIRECTED) && (pdce->DCX_flags & DCX_INUSE)) { UpdateRedirectedDC(pdce); } } } return;
case IDSYS_FADE: AnimateFade(); return;
case IDSYS_FLASHWND: xxxFlashWindow(pwnd, FLASHW_TIMERCALL, 0); return;
case IDSYS_WNDTRACKING: { /*
* If the active track window hasn't changed, * it's time to active it. * spwndTrack can be NULL if it got destroyed but we haven't * destroyed the timer.yet */ PTHREADINFO pti = GETPTI(pwnd); UserAssert(TestUP(ACTIVEWINDOWTRACKING));
if ((pti->rpdesk->spwndTrack != NULL) && (pwnd == GetActiveTrackPwnd(pti->rpdesk->spwndTrack, NULL))) {
pti->pq->QF_flags |= (QF_ACTIVEWNDTRACKING | QF_MOUSEMOVED);
#ifdef REDIRECTION
/*
* Should we call the hit test hook here ? */ PushMouseMove(pti->pq, gpsi->ptCursor); #endif // REDIRECTION
SetWakeBit(pti, QS_MOUSEMOVE); } } break;
case IDSYS_MOUSEHOVER: { PTHREADINFO pti = GETPTI(pwnd); PDESKTOP pdesk = pti->rpdesk; /*
* If hover hasn't been canceled, the mouse is still on * this window and the point is still on the rect, then * it's hover time! */ if ((pdesk->dwDTFlags & DF_TRACKMOUSEHOVER) && (HWq(pwnd) == HWq(pdesk->spwndTrack) && PtInRect(&pdesk->rcMouseHover, gpsi->ptCursor))) {
UINT message; WPARAM wParam; POINT pt = gpsi->ptCursor;
if (pdesk->htEx == HTCLIENT) { message = WM_MOUSEHOVER; wParam = (WPARAM)GetMouseKeyFlags(pti->pq); if (TestWF(pwnd, WEFLAYOUTRTL)) { pt.x = pwnd->rcClient.right - pt.x - 1; } else { pt.x -= pwnd->rcClient.left; } pt.y -= pwnd->rcClient.top; } else { message = WM_NCMOUSEHOVER; /*
* Map the extended hit test code to a public one. */ wParam = (WPARAM)LOWORD(pdesk->htEx); if ((wParam >= HTEXMENUFIRST) && (wParam <= HTEXMENULAST)) { wParam = (WPARAM)HTMENU; } else if ((wParam >= HTEXSCROLLFIRST) && (wParam <= HTEXSCROLLLAST)) { wParam = (WPARAM)(HIWORD(pdesk->htEx) ? HTVSCROLL : HTHSCROLL); } }
_PostMessage(pwnd, message, wParam, MAKELPARAM(pt.x, pt.y));
pdesk->dwDTFlags &= ~DF_TRACKMOUSEHOVER; break; } } return;
default: RIPMSG1(RIP_ERROR, "xxxSystemTimerProc: unexpected id: 0x%x", id); break; }
/*
* If we fell through, the timer's got to go. */ _KillSystemTimer(pwnd, id); }
|