1 /*
2  *  mytm_sdl.cpp
3 
4 	Copyright (C) 2001 and beyond by Woody Zenfell, III
5 	and the "Aleph One" developers.
6 
7 	This program is free software; you can redistribute it and/or modify
8 	it under the terms of the GNU General Public License as published by
9 	the Free Software Foundation; either version 3 of the License, or
10 	(at your option) any later version.
11 
12 	This program is distributed in the hope that it will be useful,
13 	but WITHOUT ANY WARRANTY; without even the implied warranty of
14 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 	GNU General Public License for more details.
16 
17 	This license is contained in the file "COPYING",
18 	which is included with this source code; it is available online at
19 	http://www.gnu.org/licenses/gpl.html
20 
21  *  The point of this file is to let us (networking code, in particular) use timing services
22  *  with the same source-level API wrapper that Bungie used to access the Mac's Time Manager.
23  *
24  *  Created by woody on Mon Oct 15 2001.
25  *
26  *  3 December 2001 (Woody Zenfell): changed dependence on SDL_Threadx's SetRelativeThreadPriority
27  *	to simply BoostThreadPriority(), a simpler function with a simpler interface.
28  *
29  *  14 January 2003 (Woody Zenfell): TMTasks lock each other out while running (better models
30  *      Time Manager behavior, so makes code safer).  Also removed missedDeadline stuff.
31  */
32 
33 // The implementation is built on SDL_thread, and approximates the Time Manager behavior.
34 // Obviously, it's not a perfect emulation.  :)
35 // In particular, though TMTasks now lock one another out (as they should), TMTasks do not
36 // (cannot?) effectively lock out the main thread (as they would in Mac OS 9)... but, their
37 // threads ought to be higher-priority than the main thread, which means that as long as they
38 // don't block (which they shouldn't anyway), the main thread will not run while they do.
39 
40 // I probably would have made life easier for myself by using SDL_timer instead, but frankly
41 // the documentation does not inspire me to trust it.  I'll do things on my own.
42 
43 #include "cseries.h"
44 #include "thread_priority_sdl.h"
45 #include "mytm.h"
46 
47 #include <vector>
48 
49 #include "SDL_thread.h"
50 #include "SDL_timer.h"
51 #include "SDL_error.h"
52 
53 #include "Logging.h"
54 
55 #ifndef NO_STD_NAMESPACE
56 using std::vector;
57 #endif
58 
59 #ifdef DEBUG
60 struct myTMTask_profile {
61     uint32		mStartTime;
62     uint32		mFinishTime;
63     uint32		mNumCallsThisReset;
64     uint32		mNumCallsTotal;
65     int32		mDriftMin;
66     int32		mDriftMax;
67     uint32		mNumLateCalls;
68     uint32		mNumWarmResets;
69     uint32		mNumResuscitations;
70 };
71 #endif
72 
73 // Housekeeping structure used in setup, teardown, and execution
74 struct myTMTask {
75     SDL_Thread*		mThread;
76     uint32		mPeriod;
77     bool 		(*mFunction)(void);
78     volatile bool	mKeepRunning;	// set true by myTMSetup; set false by thread or by myTMRemove.
79     volatile bool	mIsRunning;	// set true by myTMSetup; set false by thread when about to exit.
80     volatile uint32	mResetTime;	// set positive by myTMReset; set to 0 by myTMSetup or by thread.
81 #ifdef DEBUG
82     myTMTask_profile	mProfilingData;
83 #endif
84 };
85 
86 // Only one TMTask should be scheduled at any given time, so they take this mutex.
87 static SDL_mutex* sTMTaskMutex = NULL;
88 
89 
90 void
mytm_initialize()91 mytm_initialize() {
92     // XXX should provide a way to destroy the mutex too - currently we rely on process exit to do that.
93     if(sTMTaskMutex == NULL) {
94         sTMTaskMutex = SDL_CreateMutex();
95 
96         //logCheckWarn0(sTMTaskMutex != NULL, "unable to create mytm mutex lock");
97         if(sTMTaskMutex == NULL)
98             logWarning("unable to create mytm mutex lock");
99     }
100     else
101         logAnomaly("multiple invocations of mytm_initialize()");
102 }
103 
104 
105 // The logging system is not (currently) thread-safe, so these logging calls are potentially a Bad Idea
106 // but if something's going wrong already, maybe it wouldn't hurt to take a small risk to shed some light.
107 bool
take_mytm_mutex()108 take_mytm_mutex() {
109     bool success = (SDL_LockMutex(sTMTaskMutex) != -1);
110     if(!success)
111         logAnomaly("take_mytm_mutex(): SDL_LockMutex() failed: %s", SDL_GetError());
112     return success;
113 }
114 
115 
116 
117 bool
release_mytm_mutex()118 release_mytm_mutex() {
119     bool success = (SDL_UnlockMutex(sTMTaskMutex) != -1);
120     if(!success)
121         logAnomaly("release_mytm_mutex(): SDL_UnlockMutex() failed: %s", SDL_GetError());
122     return success;
123 }
124 
125 
126 
127 // Function that threads execute - does housekeeping and calls user callback
128 // Tries to be drift-free.
129 static int
thread_loop(void * inData)130 thread_loop(void* inData) {
131     myTMTask*	theTMTask	= (myTMTask*) inData;
132 
133     uint32	theLastRunTime	= SDL_GetTicks();
134     uint32	theCurrentRunTime;
135     int32	theDrift	= 0;
136 
137 #ifdef DEBUG
138     theTMTask->mProfilingData.mStartTime	= theLastRunTime;
139 #endif
140 
141     while(theTMTask->mKeepRunning) {
142         // Delay, unless we're at least a period behind schedule
143         // Originally, I didn't compute theDelay explicitly as a signed quantity, which
144         // made for some VERY long waits if we were running late...
145         int32	theDelay 	= theTMTask->mPeriod - theDrift;
146         if(theDelay > 0)
147             SDL_Delay(theDelay);
148         else {
149             // We missed a deadline!
150 #ifdef DEBUG
151             theTMTask->mProfilingData.mNumLateCalls++;
152 #endif
153         }
154 
155         // If a reset was requested, pretend we were last called at the reset time, clear the reset,
156         // and delay some more if needed.
157         // Note: this is a "while" so, in case another reset comes while we are in the Delay() inside
158         // this block, we wait longer.
159         while(theTMTask->mResetTime > 0) {
160             theLastRunTime	= theTMTask->mResetTime;
161             theTMTask->mResetTime = 0;
162 
163 #ifdef DEBUG
164             theTMTask->mProfilingData.mNumWarmResets++;
165             theTMTask->mProfilingData.mStartTime		= theLastRunTime;
166             theTMTask->mProfilingData.mNumCallsThisReset	= 0;
167 #endif
168 
169             theCurrentRunTime	= SDL_GetTicks();
170             theDrift		+= theCurrentRunTime - theLastRunTime - theTMTask->mPeriod;
171             theLastRunTime	= theCurrentRunTime;
172 
173 #ifdef DEBUG
174             if(theDrift < theTMTask->mProfilingData.mDriftMin)
175                 theTMTask->mProfilingData.mDriftMin	= theDrift;
176 
177             if(theDrift > theTMTask->mProfilingData.mDriftMax)
178                 theTMTask->mProfilingData.mDriftMax	= theDrift;
179 #endif
180 
181             theDelay = theTMTask->mPeriod - theDrift;
182 
183             if(theDelay > 0)
184                 SDL_Delay(theDelay);
185             else {
186                 // We did miss a deadline!
187 #ifdef DEBUG
188                 theTMTask->mProfilingData.mNumLateCalls++;
189 #endif
190             }
191         }
192 
193 	theCurrentRunTime	= SDL_GetTicks();
194 	theDrift		+= theCurrentRunTime - theLastRunTime - theTMTask->mPeriod;
195         theLastRunTime		= theCurrentRunTime;
196 
197 #ifdef DEBUG
198         if(theDrift < theTMTask->mProfilingData.mDriftMin)
199             theTMTask->mProfilingData.mDriftMin	= theDrift;
200 
201         if(theDrift > theTMTask->mProfilingData.mDriftMax)
202             theTMTask->mProfilingData.mDriftMax	= theDrift;
203 #endif
204 
205         // Since we've been delayed for a while, double-check that we still want to run.
206         if(theTMTask->mKeepRunning == false)
207             break;
208 
209         // NOTE: since we could be preempted between checking for termination and actually calling the
210         // callback, there is a VERY small chance that mFunction could be called (at most once) after
211         // myTMRemoveTask() completes.  This is a BUG, but to avoid expensive synchronization (making
212         // myTMRemoveTask() block until this thread finishes, protecting mKeepRunning with a mutex, etc.)
213         // we take our chances.  This bug could only bite anyway (in the current IPring) while making the
214         // transition from a normal player to the gatherer (in drop_upring_player()) as a result of the
215         // gatherer becoming netdead - not terribly likely to begin with!
216 
217         // Call the function.  If it doesn't want to be rescheduled, stop ourselves.
218 #ifdef DEBUG
219         theTMTask->mProfilingData.mNumCallsThisReset++;
220         theTMTask->mProfilingData.mNumCallsTotal++;
221 #endif
222 
223         bool runAgain = true;
224 
225         // Lock out other tmtasks while we run ours
226         if(take_mytm_mutex()) {
227             runAgain = theTMTask->mFunction();
228             release_mytm_mutex();
229         }
230 
231         if(!runAgain)
232             break;
233     }
234 
235     theTMTask->mIsRunning = false;
236 
237 #ifdef DEBUG
238     theTMTask->mProfilingData.mFinishTime	= SDL_GetTicks();
239 #endif
240 
241     return 0;
242 }
243 
244 
245 static vector<myTMTaskPtr> sOutstandingTasks;
246 
247 
248 // Set up a periodic callout with no anti-drift mechanisms.  (We don't support that,
249 // but it's unlikely that anyone is counting on NOT having drift-correction?)
250 myTMTaskPtr
myTMSetup(int32 time,bool (* func)(void))251 myTMSetup(int32 time, bool (*func)(void)) {
252     return myXTMSetup(time, func);
253 }
254 
255 // Set up a periodic callout, with what tries to be a fairly drift-free period.
256 myTMTaskPtr
myXTMSetup(int32 time,bool (* func)(void))257 myXTMSetup(int32 time, bool (*func)(void)) {
258     myTMTaskPtr	theTask = new myTMTask;
259 
260     theTask->mPeriod		= time;
261     theTask->mFunction		= func;
262     theTask->mKeepRunning	= true;
263     theTask->mIsRunning		= true;
264     theTask->mResetTime		= 0;
265 
266 #ifdef DEBUG
267     obj_clear(theTask->mProfilingData);
268 #endif
269 
270     theTask->mThread		= SDL_CreateThread(thread_loop, "myXTMSetup_taskThread", theTask);
271 
272     // Set thread priority a little higher
273     BoostThreadPriority(theTask->mThread);
274 
275     sOutstandingTasks.push_back(theTask);
276 
277     return theTask;
278 }
279 
280 // Stop an existing callout from executing.
281 myTMTaskPtr
myTMRemove(myTMTaskPtr task)282 myTMRemove(myTMTaskPtr task) {
283     if(task != NULL)
284         task->mKeepRunning	= false;
285 
286     return NULL;
287 }
288 
289 // Reset an existing callout's delay to the original value.
290 // This is similar to myTMRemove() followed by another myTMSetup() with the same task and period.
291 void
myTMReset(myTMTaskPtr task)292 myTMReset(myTMTaskPtr task) {
293     if(task != NULL) {
294         // If the thread has not exited, we can message it.  NOTE: there is a small possibility
295         // that the thread has already broken its loop, but got preempted before it could set
296         // mIsRunning to false.  I'm going to take the easy lazy evil way out and just hope that
297         // doesn't happen.
298         if(task->mIsRunning)
299             task->mResetTime	= SDL_GetTicks();
300 
301         // Otherwise, we need to start a new thread for the task.
302         else {
303             // This is our only chance to clean up that zombie thread.  This should not block.
304             SDL_WaitThread(task->mThread, NULL);
305 
306             task->mKeepRunning	= true;
307             task->mIsRunning	= true;
308             task->mResetTime	= 0;
309 
310 #ifdef DEBUG
311             task->mProfilingData.mNumResuscitations++;
312             task->mProfilingData.mNumCallsThisReset = 0;
313 #endif
314 
315             task->mThread	= SDL_CreateThread(thread_loop, "myTMReset_taskThread", task);
316 
317             // Set thread priority a little higher
318             BoostThreadPriority(task->mThread);
319         }
320     }
321 }
322 
323 #ifdef DEBUG
324 // ZZZ addition (to myTM interface): dump profiling data
325 #define DUMPIT_ZU(structure,field_name) logDump("" #field_name ":\t%u", (structure).field_name)
326 #define DUMPIT_ZS(structure,field_name) logDump("" #field_name ":\t%d", (structure).field_name)
327 
328 void
myTMDumpProfile(myTMTask * inTask)329 myTMDumpProfile(myTMTask* inTask) {
330     if(inTask != NULL) {
331         logDump("PROFILE FOR SDL TMTASK %p (function %p)", inTask, inTask->mFunction);
332         DUMPIT_ZU((*inTask), mPeriod);
333         DUMPIT_ZU(inTask->mProfilingData, mStartTime);
334         DUMPIT_ZU(inTask->mProfilingData, mFinishTime);
335         DUMPIT_ZU(inTask->mProfilingData, mNumCallsThisReset);
336         DUMPIT_ZU(inTask->mProfilingData, mNumCallsTotal);
337         DUMPIT_ZS(inTask->mProfilingData, mDriftMin);
338         DUMPIT_ZS(inTask->mProfilingData, mDriftMax);
339         DUMPIT_ZU(inTask->mProfilingData, mNumLateCalls);
340         DUMPIT_ZU(inTask->mProfilingData, mNumWarmResets);
341         DUMPIT_ZU(inTask->mProfilingData, mNumResuscitations);
342     }
343 }
344 #endif//DEBUG
345 
346 // ZZZ addition: clean up outstanding timer task blocks and threads
347 // This could be slightly more efficient maybe by using a list, condensing calls to erase(), etc...
348 // but why bother?  It's only used occasionally at non-time-critical moments, and we're only dealing with
349 // a small handful of (small) elements anyway.
350 void
myTMCleanup(bool inWaitForFinishers)351 myTMCleanup(bool inWaitForFinishers) {
352     auto i = sOutstandingTasks.begin();
353     while (i != sOutstandingTasks.end()) {
354         if((*i)->mKeepRunning == false && (inWaitForFinishers || (*i)->mIsRunning == false)) {
355             myTMTaskPtr	theDeadTask = *i;
356             auto next_i = sOutstandingTasks.erase(i);
357             i = next_i;
358 
359 #ifdef DEBUG
360             myTMDumpProfile(theDeadTask);
361 #endif
362 
363             SDL_WaitThread(theDeadTask->mThread, NULL);
364             delete theDeadTask;
365         }
366         else
367             ++i; // skip task
368     }
369 }
370