Thread creation and destruction is now handled by the main loop.
Some modifications to the initialization code were also necessary to keep the system from deadlocking itself immediately; only subthreads may now call CreateThread or AssignTask. All threads are guaranteed to get a WaitThread () on them when they're finished now, which hopefully will stop thread ID leaking (bug #561). git-svn-id: svn://svn.code.sf.net/p/sc2/code/trunk@1311 8092fc87-c524-0410-9efc-e669fe64eaf9
This commit is contained in:
@@ -1,4 +1,7 @@
|
|||||||
Changes towards version 0.4:
|
Changes towards version 0.4:
|
||||||
|
- Thread code refactoring: only the main thread will actually spawn
|
||||||
|
threads, and thread IDs are properly recycled with SDL_WaitThead ()
|
||||||
|
once they're done. (With luck, this will fix bug #561) -Michael
|
||||||
- Sound code refactoring: core api is now virtualized,
|
- Sound code refactoring: core api is now virtualized,
|
||||||
MixSDL is divided to generic mixer and driver entities - Mika
|
MixSDL is divided to generic mixer and driver entities - Mika
|
||||||
- Optimized MixSDL mixing and resampling routines
|
- Optimized MixSDL mixing and resampling routines
|
||||||
|
|||||||
@@ -77,6 +77,11 @@ enum
|
|||||||
SYNC_CLASS_RESOURCE = (1 << 3) /* Involves system resources (_MemoryLock) */
|
SYNC_CLASS_RESOURCE = (1 << 3) /* Involves system resources (_MemoryLock) */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* Note. NEVER call CreateThread from the main thread, or deadlocks
|
||||||
|
are guaranteed. Use StartThread instead (which doesn't wait around
|
||||||
|
for the main thread to actually create the thread and return
|
||||||
|
it). */
|
||||||
|
|
||||||
#ifdef NAMED_SYNCHRO
|
#ifdef NAMED_SYNCHRO
|
||||||
/* Logical OR of all classes we want to track. */
|
/* Logical OR of all classes we want to track. */
|
||||||
#define TRACK_CONTENTION_CLASSES (SYNC_CLASS_TOPLEVEL)
|
#define TRACK_CONTENTION_CLASSES (SYNC_CLASS_TOPLEVEL)
|
||||||
@@ -84,6 +89,7 @@ enum
|
|||||||
/* Prototypes with the "name" field */
|
/* Prototypes with the "name" field */
|
||||||
|
|
||||||
Thread CreateThread_Core (ThreadFunction func, void *data, SDWORD stackSize, const char *name);
|
Thread CreateThread_Core (ThreadFunction func, void *data, SDWORD stackSize, const char *name);
|
||||||
|
void StartThread_Core (ThreadFunction func, void *data, SDWORD stackSize, const char *name);
|
||||||
Semaphore CreateSemaphore_Core (DWORD initial, const char *name, DWORD syncClass);
|
Semaphore CreateSemaphore_Core (DWORD initial, const char *name, DWORD syncClass);
|
||||||
Mutex CreateMutex_Core (const char *name, DWORD syncClass);
|
Mutex CreateMutex_Core (const char *name, DWORD syncClass);
|
||||||
RecursiveMutex CreateRecursiveMutex_Core (const char *name, DWORD syncClass);
|
RecursiveMutex CreateRecursiveMutex_Core (const char *name, DWORD syncClass);
|
||||||
@@ -93,6 +99,8 @@ CondVar CreateCondVar_Core (const char *name, DWORD syncClass);
|
|||||||
|
|
||||||
#define CreateThread(func, data, stackSize, name) \
|
#define CreateThread(func, data, stackSize, name) \
|
||||||
CreateThread_Core ((func), (data), (stackSize), (name))
|
CreateThread_Core ((func), (data), (stackSize), (name))
|
||||||
|
#define StartThread(func, data, stackSize, name) \
|
||||||
|
StartThread_Core ((func), (data), (stackSize), (name))
|
||||||
#define CreateSemaphore(initial, name, syncClass) \
|
#define CreateSemaphore(initial, name, syncClass) \
|
||||||
CreateSemaphore_Core ((initial), (name), (syncClass))
|
CreateSemaphore_Core ((initial), (name), (syncClass))
|
||||||
#define CreateMutex(name, syncClass) \
|
#define CreateMutex(name, syncClass) \
|
||||||
@@ -106,6 +114,7 @@ CondVar CreateCondVar_Core (const char *name, DWORD syncClass);
|
|||||||
|
|
||||||
/* Prototypes without the "name" field. */
|
/* Prototypes without the "name" field. */
|
||||||
Thread CreateThread_Core (ThreadFunction func, void *data, SDWORD stackSize);
|
Thread CreateThread_Core (ThreadFunction func, void *data, SDWORD stackSize);
|
||||||
|
void StartThread_Core (ThreadFunction func, void *data, SDWORD stackSize);
|
||||||
Semaphore CreateSemaphore_Core (DWORD initial);
|
Semaphore CreateSemaphore_Core (DWORD initial);
|
||||||
Mutex CreateMutex_Core (void);
|
Mutex CreateMutex_Core (void);
|
||||||
RecursiveMutex CreateRecursiveMutex_Core (void);
|
RecursiveMutex CreateRecursiveMutex_Core (void);
|
||||||
@@ -117,6 +126,8 @@ CondVar CreateCondVar_Core (void);
|
|||||||
|
|
||||||
#define CreateThread(func, data, stackSize, name) \
|
#define CreateThread(func, data, stackSize, name) \
|
||||||
CreateThread_Core ((func), (data), (stackSize))
|
CreateThread_Core ((func), (data), (stackSize))
|
||||||
|
#define StartThread(func, data, stackSize, name) \
|
||||||
|
StartThread_Core ((func), (data), (stackSize))
|
||||||
#define CreateSemaphore(initial, name, syncClass) \
|
#define CreateSemaphore(initial, name, syncClass) \
|
||||||
CreateSemaphore_Core ((initial))
|
CreateSemaphore_Core ((initial))
|
||||||
#define CreateMutex(name, syncClass) \
|
#define CreateMutex(name, syncClass) \
|
||||||
@@ -134,9 +145,13 @@ ThreadLocal *GetMyThreadLocal (void);
|
|||||||
|
|
||||||
void SleepThread (TimePeriod timePeriod);
|
void SleepThread (TimePeriod timePeriod);
|
||||||
void SleepThreadUntil (TimeCount wakeTime);
|
void SleepThreadUntil (TimeCount wakeTime);
|
||||||
|
void DestroyThread (Thread);
|
||||||
void TaskSwitch (void);
|
void TaskSwitch (void);
|
||||||
void WaitThread (Thread thread, int *status);
|
void WaitThread (Thread thread, int *status);
|
||||||
|
|
||||||
|
void FinishThread (Thread);
|
||||||
|
void ProcessThreadLifecycles (void);
|
||||||
|
|
||||||
#ifdef PROFILE_THREADS
|
#ifdef PROFILE_THREADS
|
||||||
void PrintThreadsStats (void);
|
void PrintThreadsStats (void);
|
||||||
#endif /* PROFILE_THREADS */
|
#endif /* PROFILE_THREADS */
|
||||||
|
|||||||
@@ -220,10 +220,17 @@ ThreadHelper (void *startInfo) {
|
|||||||
|
|
||||||
UnQueueThread (thread);
|
UnQueueThread (thread);
|
||||||
DestroyThreadLocal (thread->localData);
|
DestroyThreadLocal (thread->localData);
|
||||||
HFree (thread);
|
FinishThread (thread);
|
||||||
|
/* Destroying the thread is the responsibility of ProcessThreadLifecycles() */
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
DestroyThread_SDL (Thread t)
|
||||||
|
{
|
||||||
|
HFree (t);
|
||||||
|
}
|
||||||
|
|
||||||
Thread
|
Thread
|
||||||
CreateThread_SDL (ThreadFunction func, void *data, SDWORD stackSize
|
CreateThread_SDL (ThreadFunction func, void *data, SDWORD stackSize
|
||||||
#ifdef NAMED_SYNCHRO
|
#ifdef NAMED_SYNCHRO
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ void SleepThread_SDL (TimeCount sleepTime);
|
|||||||
void SleepThreadUntil_SDL (TimeCount wakeTime);
|
void SleepThreadUntil_SDL (TimeCount wakeTime);
|
||||||
void TaskSwitch_SDL (void);
|
void TaskSwitch_SDL (void);
|
||||||
void WaitThread_SDL (Thread thread, int *status);
|
void WaitThread_SDL (Thread thread, int *status);
|
||||||
|
void DestroyThread_SDL (Thread thread);
|
||||||
|
|
||||||
void DestroyMutex_SDL (Mutex m);
|
void DestroyMutex_SDL (Mutex m);
|
||||||
void LockMutex_SDL (Mutex m);
|
void LockMutex_SDL (Mutex m);
|
||||||
@@ -79,6 +80,7 @@ int GetRecursiveMutexDepth_SDL (RecursiveMutex m);
|
|||||||
#define NativeSleepThreadUntil SleepThreadUntil_SDL
|
#define NativeSleepThreadUntil SleepThreadUntil_SDL
|
||||||
#define NativeTaskSwitch TaskSwitch_SDL
|
#define NativeTaskSwitch TaskSwitch_SDL
|
||||||
#define NativeWaitThread WaitThread_SDL
|
#define NativeWaitThread WaitThread_SDL
|
||||||
|
#define NativeDestroyThread DestroyThread_SDL
|
||||||
|
|
||||||
#define NativeCreateMutex CreateMutex_SDL
|
#define NativeCreateMutex CreateMutex_SDL
|
||||||
#define NativeDestroyMutex DestroyMutex_SDL
|
#define NativeDestroyMutex DestroyMutex_SDL
|
||||||
|
|||||||
@@ -18,23 +18,140 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
#include "libs/threadlib.h"
|
#include "libs/threadlib.h"
|
||||||
#include "libs/timelib.h"
|
#include "libs/timelib.h"
|
||||||
#include "libs/misc.h"
|
#include "libs/misc.h"
|
||||||
#include "thrcommon.h"
|
#include "thrcommon.h"
|
||||||
|
|
||||||
|
#define LIFECYCLE_SIZE 8
|
||||||
|
typedef struct {
|
||||||
|
ThreadFunction func;
|
||||||
|
void *data;
|
||||||
|
SDWORD stackSize;
|
||||||
|
Semaphore sem;
|
||||||
|
Thread value;
|
||||||
|
#ifdef NAMED_SYNCHRO
|
||||||
|
const char *name;
|
||||||
|
#endif
|
||||||
|
} SpawnRequest_struct;
|
||||||
|
|
||||||
|
typedef SpawnRequest_struct *SpawnRequest;
|
||||||
|
|
||||||
|
static Mutex lifecycleMutex;
|
||||||
|
static SpawnRequest pendingBirth[LIFECYCLE_SIZE];
|
||||||
|
static Thread pendingDeath[LIFECYCLE_SIZE];
|
||||||
|
|
||||||
void
|
void
|
||||||
InitThreadSystem (void)
|
InitThreadSystem (void)
|
||||||
{
|
{
|
||||||
|
int i;
|
||||||
NativeInitThreadSystem ();
|
NativeInitThreadSystem ();
|
||||||
|
for (i = 0; i < LIFECYCLE_SIZE; i++)
|
||||||
|
{
|
||||||
|
pendingBirth[i] = NULL;
|
||||||
|
pendingDeath[i] = NULL;
|
||||||
|
}
|
||||||
|
lifecycleMutex = CreateMutex ("Thread Lifecycle Mutex", SYNC_CLASS_RESOURCE);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
UnInitThreadSystem (void)
|
UnInitThreadSystem (void)
|
||||||
{
|
{
|
||||||
NativeUnInitThreadSystem ();
|
NativeUnInitThreadSystem ();
|
||||||
|
DestroyMutex (lifecycleMutex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Thread
|
||||||
|
FlagStartThread (SpawnRequest s)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
LockMutex (lifecycleMutex);
|
||||||
|
for (i = 0; i < LIFECYCLE_SIZE; i++)
|
||||||
|
{
|
||||||
|
if (pendingBirth[i] == NULL)
|
||||||
|
{
|
||||||
|
pendingBirth[i] = s;
|
||||||
|
UnlockMutex (lifecycleMutex);
|
||||||
|
if (s->sem)
|
||||||
|
{
|
||||||
|
Thread result;
|
||||||
|
SetSemaphore (s->sem);
|
||||||
|
DestroySemaphore (s->sem);
|
||||||
|
result = s->value;
|
||||||
|
HFree (s);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fprintf (stderr, "Thread Lifecycle array filled. This is a fatal error! Make LIFECYCLE_SIZE something larger than %d.\n", LIFECYCLE_SIZE);
|
||||||
|
exit (-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
FinishThread (Thread thread)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
LockMutex (lifecycleMutex);
|
||||||
|
for (i = 0; i < LIFECYCLE_SIZE; i++)
|
||||||
|
{
|
||||||
|
if (pendingDeath[i] == NULL)
|
||||||
|
{
|
||||||
|
pendingDeath[i] = thread;
|
||||||
|
UnlockMutex (lifecycleMutex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fprintf (stderr, "Thread Lifecycle array filled. This is a fatal error! Make LIFECYCLE_SIZE something larger than %d.\n", LIFECYCLE_SIZE);
|
||||||
|
exit (-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Only call from main thread! */
|
||||||
|
void
|
||||||
|
ProcessThreadLifecycles (void)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
LockMutex (lifecycleMutex);
|
||||||
|
for (i = 0; i < LIFECYCLE_SIZE; i++)
|
||||||
|
{
|
||||||
|
SpawnRequest s = pendingBirth[i];
|
||||||
|
if (s != NULL)
|
||||||
|
{
|
||||||
|
#ifdef NAMED_SYNCHRO
|
||||||
|
s->value = NativeCreateThread (s->func, s->data, s->stackSize, s->name);
|
||||||
|
#else
|
||||||
|
s->value = NativeCreateThread (s->func, s->data, s->stackSize);
|
||||||
|
#endif
|
||||||
|
if (s->sem)
|
||||||
|
{
|
||||||
|
ClearSemaphore (s->sem);
|
||||||
|
/* The spawning thread's FlagStartThread will clean up s */
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* The thread value has been lost to the game logic. We must
|
||||||
|
clean up s ourself. */
|
||||||
|
HFree (s);
|
||||||
|
}
|
||||||
|
pendingBirth[i] = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < LIFECYCLE_SIZE; i++)
|
||||||
|
{
|
||||||
|
Thread t = pendingDeath[i];
|
||||||
|
if (t != NULL)
|
||||||
|
{
|
||||||
|
WaitThread (t, NULL);
|
||||||
|
pendingDeath[i] = NULL;
|
||||||
|
DestroyThread (t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UnlockMutex (lifecycleMutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* The Create routines look different based on whether NAMED_SYNCHRO
|
/* The Create routines look different based on whether NAMED_SYNCHRO
|
||||||
is defined or not. */
|
is defined or not. */
|
||||||
|
|
||||||
@@ -42,7 +159,25 @@ UnInitThreadSystem (void)
|
|||||||
Thread
|
Thread
|
||||||
CreateThread_Core (ThreadFunction func, void *data, SDWORD stackSize, const char *name)
|
CreateThread_Core (ThreadFunction func, void *data, SDWORD stackSize, const char *name)
|
||||||
{
|
{
|
||||||
return NativeCreateThread (func, data, stackSize, name);
|
SpawnRequest s = HMalloc(sizeof (SpawnRequest_struct));
|
||||||
|
s->func = func;
|
||||||
|
s->data = data;
|
||||||
|
s->stackSize = stackSize;
|
||||||
|
s->name = name;
|
||||||
|
s->sem = CreateSemaphore (0, "SpawnRequest semaphore", SYNC_CLASS_RESOURCE);
|
||||||
|
return FlagStartThread (s);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
StartThread_Core (ThreadFunction func, void *data, SDWORD stackSize, const char *name)
|
||||||
|
{
|
||||||
|
SpawnRequest s = HMalloc(sizeof (SpawnRequest_struct));
|
||||||
|
s->func = func;
|
||||||
|
s->data = data;
|
||||||
|
s->stackSize = stackSize;
|
||||||
|
s->name = name;
|
||||||
|
s->sem = NULL;
|
||||||
|
FlagStartThread (s);
|
||||||
}
|
}
|
||||||
|
|
||||||
Mutex
|
Mutex
|
||||||
@@ -74,7 +209,23 @@ CreateCondVar_Core (const char *name, DWORD syncClass)
|
|||||||
Thread
|
Thread
|
||||||
CreateThread_Core (ThreadFunction func, void *data, SDWORD stackSize)
|
CreateThread_Core (ThreadFunction func, void *data, SDWORD stackSize)
|
||||||
{
|
{
|
||||||
return NativeCreateThread (func, data, stackSize);
|
SpawnRequest s = HMalloc(sizeof (SpawnRequest_struct));
|
||||||
|
s->func = func;
|
||||||
|
s->data = data;
|
||||||
|
s->stackSize = stackSize;
|
||||||
|
s->sem = CreateSemaphore (0, "SpawnRequest semaphore", SYNC_CLASS_RESOURCE);
|
||||||
|
return FlagStartThread (s);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
StartThread_Core (ThreadFunction func, void *data, SDWORD stackSize)
|
||||||
|
{
|
||||||
|
SpawnRequest s = HMalloc(sizeof (SpawnRequest_struct));
|
||||||
|
s->func = func;
|
||||||
|
s->data = data;
|
||||||
|
s->stackSize = stackSize;
|
||||||
|
s->sem = NULL;
|
||||||
|
FlagStartThread (s);
|
||||||
}
|
}
|
||||||
|
|
||||||
Mutex
|
Mutex
|
||||||
@@ -102,6 +253,12 @@ CreateCondVar_Core (void)
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
void
|
||||||
|
DestroyThread (Thread t)
|
||||||
|
{
|
||||||
|
NativeDestroyThread (t);
|
||||||
|
}
|
||||||
|
|
||||||
ThreadLocal *
|
ThreadLocal *
|
||||||
CreateThreadLocal (void)
|
CreateThreadLocal (void)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -849,6 +849,9 @@ EventHandler (BYTE selector)
|
|||||||
|
|
||||||
#define DEBUG_PSYTRON 0
|
#define DEBUG_PSYTRON 0
|
||||||
|
|
||||||
|
/* TODO: Remove these declarations once threading is gone. */
|
||||||
|
extern int snddriver, soundflags;
|
||||||
|
|
||||||
int Starcon2Main(void* blah)
|
int Starcon2Main(void* blah)
|
||||||
{
|
{
|
||||||
#if DEBUG_PSYTRON || CREATE_JOURNAL
|
#if DEBUG_PSYTRON || CREATE_JOURNAL
|
||||||
@@ -882,7 +885,10 @@ while (--ac > 0)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif //DEBUG_PSYTRON || CREATE_JOURNAL
|
#endif //DEBUG_PSYTRON || CREATE_JOURNAL
|
||||||
|
|
||||||
|
/* TODO: Put initAudio back in main where it belongs once threading is gone */
|
||||||
|
initAudio (snddriver, soundflags);
|
||||||
|
|
||||||
if (LoadKernel (0,0))
|
if (LoadKernel (0,0))
|
||||||
{
|
{
|
||||||
extern BOOLEAN StartGame (void);
|
extern BOOLEAN StartGame (void);
|
||||||
|
|||||||
+18
-4
@@ -44,6 +44,10 @@
|
|||||||
// Including this is actually necessary on OSX.
|
// Including this is actually necessary on OSX.
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/* TODO: Remove these (making them local to main() ) once threading is
|
||||||
|
gone. */
|
||||||
|
|
||||||
|
int snddriver, soundflags;
|
||||||
|
|
||||||
static int
|
static int
|
||||||
Check_PC_3DO_opt (const char *value, DWORD mask, const char *opt)
|
Check_PC_3DO_opt (const char *value, DWORD mask, const char *opt)
|
||||||
@@ -66,9 +70,7 @@ int
|
|||||||
main (int argc, char *argv[])
|
main (int argc, char *argv[])
|
||||||
{
|
{
|
||||||
int gfxdriver = TFB_GFXDRIVER_SDL_PURE;
|
int gfxdriver = TFB_GFXDRIVER_SDL_PURE;
|
||||||
int snddriver = audio_DRIVER_MIXSDL;
|
|
||||||
int gfxflags = 0;
|
int gfxflags = 0;
|
||||||
int soundflags = audio_QUALITY_MEDIUM;
|
|
||||||
int width = 640, height = 480, bpp = 16;
|
int width = 640, height = 480, bpp = 16;
|
||||||
int vol;
|
int vol;
|
||||||
int val;
|
int val;
|
||||||
@@ -120,6 +122,14 @@ main (int argc, char *argv[])
|
|||||||
{0, 0, 0, 0}
|
{0, 0, 0, 0}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* TODO: Once threading is gone, these become local variables
|
||||||
|
again. In the meantime, they must be global so that
|
||||||
|
initAudio (in StarCon2Main) can see them. initAudio needed
|
||||||
|
to be moved there because calling AssignTask in the main
|
||||||
|
thread doesn't work */
|
||||||
|
snddriver = audio_DRIVER_MIXSDL;
|
||||||
|
soundflags = audio_QUALITY_MEDIUM;
|
||||||
|
|
||||||
fprintf (stderr, "The Ur-Quan Masters v%d.%d%s (compiled %s %s)\n"
|
fprintf (stderr, "The Ur-Quan Masters v%d.%d%s (compiled %s %s)\n"
|
||||||
"This software comes with ABSOLUTELY NO WARRANTY;\n"
|
"This software comes with ABSOLUTELY NO WARRANTY;\n"
|
||||||
"for details see the included 'COPYING' file.\n\n",
|
"for details see the included 'COPYING' file.\n\n",
|
||||||
@@ -357,14 +367,18 @@ main (int argc, char *argv[])
|
|||||||
init_communication ();
|
init_communication ();
|
||||||
if (gammaset)
|
if (gammaset)
|
||||||
TFB_SetGamma (gamma);
|
TFB_SetGamma (gamma);
|
||||||
initAudio (snddriver, soundflags);
|
/* TODO: Once threading is gone, restore initAudio here.
|
||||||
|
initAudio calls AssignTask, which currently blocks on
|
||||||
|
ProcessThreadLifecycles... */
|
||||||
|
// initAudio (snddriver, soundflags);
|
||||||
TFB_InitInput (TFB_INPUTDRIVER_SDL, 0);
|
TFB_InitInput (TFB_INPUTDRIVER_SDL, 0);
|
||||||
|
|
||||||
AssignTask (Starcon2Main, 1024, "Starcon2Main");
|
StartThread (Starcon2Main, NULL, 1024, "Starcon2Main");
|
||||||
|
|
||||||
for (;;)
|
for (;;)
|
||||||
{
|
{
|
||||||
TFB_ProcessEvents ();
|
TFB_ProcessEvents ();
|
||||||
|
ProcessThreadLifecycles ();
|
||||||
TFB_FlushGraphics ();
|
TFB_FlushGraphics ();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user