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