diff options
author | Paul Gilbert | 2012-05-11 23:08:27 +1000 |
---|---|---|
committer | Paul Gilbert | 2012-05-17 20:39:55 +1000 |
commit | c6810c174e06aa85fc58808f916c6e9cd1d989ea (patch) | |
tree | fcd9662f6de3e274e1c9f48338f99b05443fd728 /common | |
parent | 452b95143007146b07e48249312f95d23af8caa3 (diff) | |
download | scummvm-rg350-c6810c174e06aa85fc58808f916c6e9cd1d989ea.tar.gz scummvm-rg350-c6810c174e06aa85fc58808f916c6e9cd1d989ea.tar.bz2 scummvm-rg350-c6810c174e06aa85fc58808f916c6e9cd1d989ea.zip |
COMMON: Moved the Tinsel Coroutine code into it's own Common class
Diffstat (limited to 'common')
-rw-r--r-- | common/coroutines.cpp | 881 | ||||
-rw-r--r-- | common/coroutines.h | 398 | ||||
-rw-r--r-- | common/module.mk | 1 |
3 files changed, 1280 insertions, 0 deletions
diff --git a/common/coroutines.cpp b/common/coroutines.cpp new file mode 100644 index 0000000000..fff6198c22 --- /dev/null +++ b/common/coroutines.cpp @@ -0,0 +1,881 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program 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. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "common/coroutines.h" +#include "common/textconsole.h" +#include "common/system.h" + +namespace Common { + +/** Helper null context instance */ +CoroContext nullContext = NULL; + +DECLARE_SINGLETON(CoroutineScheduler); + + +#if COROUTINE_DEBUG +namespace { +static int s_coroCount = 0; + +typedef Common::HashMap<Common::String, int> CoroHashMap; +static CoroHashMap *s_coroFuncs = 0; + +static void changeCoroStats(const char *func, int change) { + if (!s_coroFuncs) + s_coroFuncs = new CoroHashMap(); + + (*s_coroFuncs)[func] += change; +} + +static void displayCoroStats() { + debug("%d active coros", s_coroCount); + + // Loop over s_coroFuncs and print info about active coros + if (!s_coroFuncs) + return; + for (CoroHashMap::const_iterator it = s_coroFuncs->begin(); + it != s_coroFuncs->end(); ++it) { + if (it->_value != 0) + debug(" %3d x %s", it->_value, it->_key.c_str()); + } +} + +} +#endif + +CoroBaseContext::CoroBaseContext(const char *func) + : _line(0), _sleep(0), _subctx(0) { +#if COROUTINE_DEBUG + _funcName = func; + changeCoroStats(_funcName, +1); + s_coroCount++; +#endif +} + +CoroBaseContext::~CoroBaseContext() { +#if COROUTINE_DEBUG + s_coroCount--; + changeCoroStats(_funcName, -1); + debug("Deleting coro in %s at %p (subctx %p)", + _funcName, (void *)this, (void *)_subctx); + displayCoroStats(); +#endif + delete _subctx; +} + +//--------------------- Scheduler Class ------------------------ + +/** + * Constructor + */ +CoroutineScheduler::CoroutineScheduler() { + processList = NULL; + pFreeProcesses = NULL; + pCurrent = NULL; + +#ifdef DEBUG + // diagnostic process counters + numProcs = 0; + maxProcs = 0; +#endif + + pRCfunction = NULL; + pidCounter = 0; + + active = new PROCESS; + active->pPrevious = NULL; + active->pNext = NULL; + + reset(); +} + +/** + * Destructor + */ +CoroutineScheduler::~CoroutineScheduler() { + // Kill all running processes (i.e. free memory allocated for their state). + PROCESS *pProc = active->pNext; + while (pProc != NULL) { + delete pProc->state; + pProc->state = 0; + pProc = pProc->pNext; + } + + free(processList); + processList = NULL; + + delete active; + active = 0; + + // Clear the event list + Common::List<EVENT *>::iterator i; + for (i = _events.begin(); i != _events.end(); ++i) + delete (*i); +} + +/** + * Kills all processes and places them on the free list. + */ +void CoroutineScheduler::reset() { + +#ifdef DEBUG + // clear number of process in use + numProcs = 0; +#endif + + if (processList == NULL) { + // first time - allocate memory for process list + processList = (PROCESS *)calloc(CORO_MAX_PROCESSES, sizeof(PROCESS)); + + // make sure memory allocated + if (processList == NULL) { + error("Cannot allocate memory for process data"); + } + + // fill with garbage + memset(processList, 'S', CORO_MAX_PROCESSES * sizeof(PROCESS)); + } + + // Kill all running processes (i.e. free memory allocated for their state). + PROCESS *pProc = active->pNext; + while (pProc != NULL) { + delete pProc->state; + pProc->state = 0; + pProc->waiting = false; + pProc = pProc->pNext; + } + + // no active processes + pCurrent = active->pNext = NULL; + + // place first process on free list + pFreeProcesses = processList; + + // link all other processes after first + for (int i = 1; i <= CORO_NUM_PROCESS; i++) { + processList[i - 1].pNext = (i == CORO_NUM_PROCESS) ? NULL : processList + i; + processList[i - 1].pPrevious = (i == 1) ? active : processList + (i - 2); + } +} + + +#ifdef DEBUG +/** + * Shows the maximum number of process used at once. + */ +void CoroutineScheduler::printStats() { + debug("%i process of %i used", maxProcs, CORO_NUM_PROCESS); +} +#endif + +#ifdef DEBUG +/** + * Checks both the active and free process list to insure all the links are valid, + * and that no processes have been lost + */ +void CoroutineScheduler::CheckStack() { + Common::List<PROCESS *> pList; + + // Check both the active and free process lists + for (int i = 0; i < 2; ++i) { + PROCESS *p = (i == 0) ? active : pFreeProcesses; + + if (p != NULL) { + // Make sure the linkages are correct + while (p->pNext != NULL) { + assert(p->pNext->pPrevious == p); + pList.push_back(p); + p = p->pNext; + } + pList.push_back(p); + } + } + + // Make sure all processes are accounted for + for (int idx = 0; idx < CORO_NUM_PROCESS; idx++) { + bool found = false; + for (Common::List<PROCESS *>::iterator i = pList.begin(); i != pList.end(); ++i) { + PROCESS *pTemp = *i; + if (*i == &processList[idx]) { + found = true; + break; + } + } + + assert(found); + } +} +#endif + +/** + * Give all active processes a chance to run + */ +void CoroutineScheduler::schedule() { + // start dispatching active process list + PROCESS *pNext; + PROCESS *pProc = active->pNext; + while (pProc != NULL) { + pNext = pProc->pNext; + + if (--pProc->sleepTime <= 0) { + // process is ready for dispatch, activate it + pCurrent = pProc; + pProc->coroAddr(pProc->state, pProc->param); + + if (!pProc->state || pProc->state->_sleep <= 0) { + // Coroutine finished + pCurrent = pCurrent->pPrevious; + killProcess(pProc); + } else { + pProc->sleepTime = pProc->state->_sleep; + } + + // pCurrent may have been changed + pNext = pCurrent->pNext; + pCurrent = NULL; + } + + pProc = pNext; + } +} + +/** + * Reschedules all the processes to run again this query + */ +void CoroutineScheduler::rescheduleAll() { + assert(pCurrent); + + // Unlink current process + pCurrent->pPrevious->pNext = pCurrent->pNext; + if (pCurrent->pNext) + pCurrent->pNext->pPrevious = pCurrent->pPrevious; + + // Add process to the start of the active list + pCurrent->pNext = active->pNext; + active->pNext->pPrevious = pCurrent; + active->pNext = pCurrent; + pCurrent->pPrevious = active; +} + +/** + * If the specified process has already run on this tick, make it run + * again on the current tick. + */ +void CoroutineScheduler::reschedule(PPROCESS pReSchedProc) { + // If not currently processing the schedule list, then no action is needed + if (!pCurrent) + return; + + if (!pReSchedProc) + pReSchedProc = pCurrent; + + PPROCESS pEnd; + + // Find the last process in the list. + // But if the target process is down the list from here, do nothing + for (pEnd = pCurrent; pEnd->pNext != NULL; pEnd = pEnd->pNext) { + if (pEnd->pNext == pReSchedProc) + return; + } + + assert(pEnd->pNext == NULL); + + // Could be in the middle of a KillProc()! + // Dying process was last and this process was penultimate + if (pReSchedProc->pNext == NULL) + return; + + // If we're moving the current process, move it back by one, so that the next + // schedule() iteration moves to the now next one + if (pCurrent == pReSchedProc) + pCurrent = pCurrent->pPrevious; + + // Unlink the process, and add it at the end + pReSchedProc->pPrevious->pNext = pReSchedProc->pNext; + pReSchedProc->pNext->pPrevious = pReSchedProc->pPrevious; + pEnd->pNext = pReSchedProc; + pReSchedProc->pPrevious = pEnd; + pReSchedProc->pNext = NULL; +} + +/** + * Moves the specified process to the end of the dispatch queue + * allowing it to run again within the current game cycle. + * @param pGiveProc Which process + */ +void CoroutineScheduler::giveWay(PPROCESS pReSchedProc) { + // If not currently processing the schedule list, then no action is needed + if (!pCurrent) + return; + + if (!pReSchedProc) + pReSchedProc = pCurrent; + + // If the process is already at the end of the queue, nothing has to be done + if (!pReSchedProc->pNext) + return; + + PPROCESS pEnd; + + // Find the last process in the list. + for (pEnd = pCurrent; pEnd->pNext != NULL; pEnd = pEnd->pNext) + ; + assert(pEnd->pNext == NULL); + + + // If we're moving the current process, move it back by one, so that the next + // schedule() iteration moves to the now next one + if (pCurrent == pReSchedProc) + pCurrent = pCurrent->pPrevious; + + // Unlink the process, and add it at the end + pReSchedProc->pPrevious->pNext = pReSchedProc->pNext; + pReSchedProc->pNext->pPrevious = pReSchedProc->pPrevious; + pEnd->pNext = pReSchedProc; + pReSchedProc->pPrevious = pEnd; + pReSchedProc->pNext = NULL; +} + +/** + * Continously makes a given process wait for another process to finish or event to signal. + * + * @param pid Process/Event identifier + * @param duration Duration in milliseconds + * @param expired If specified, set to true if delay period expired + */ +void CoroutineScheduler::waitForSingleObject(CORO_PARAM, int pid, uint32 duration, bool *expired) { + if (!pCurrent) + error("Called CoroutineScheduler::waitForSingleObject from the main process"); + + CORO_BEGIN_CONTEXT; + uint32 endTime; + PROCESS *pProcess; + EVENT *pEvent; + CORO_END_CONTEXT(_ctx); + + CORO_BEGIN_CODE(_ctx); + + // Signal as waiting + pCurrent->waiting = true; + + _ctx->endTime = (duration == CORO_INFINITE) ? CORO_INFINITE : g_system->getMillis() + duration; + if (expired) + // Presume it will expire + *expired = true; + + // Outer loop for doing checks until expiry + while (g_system->getMillis() < _ctx->endTime) { + // Check to see if a process or event with the given Id exists + _ctx->pProcess = getProcess(pid); + _ctx->pEvent = !_ctx->pProcess ? getEvent(pid) : NULL; + + // If there's no active process or event, presume it's a process that's finished, + // so the waiting can immediately exit + if ((_ctx->pProcess == NULL) && (_ctx->pEvent == NULL)) { + if (expired) + *expired = false; + break; + } + + // If a process was found, don't go into the if statement, and keep waiting. + // Likewise if it's an event that's not yet signalled + if ((_ctx->pEvent != NULL) && _ctx->pEvent->signalled) { + // Unless the event is flagged for manual reset, reset it now + if (!_ctx->pEvent->manualReset) + _ctx->pEvent->signalled = false; + + if (expired) + *expired = false; + break; + } + + // Sleep until the next cycle + CORO_SLEEP(1); + } + + // Signal waiting is done + pCurrent->waiting = false; + + CORO_END_CODE; +} + +/** + * Continously makes a given process wait for given prcesses to finished or events to be set + * + * @param nCount Number of Id's being passed + * @param evtList List of pids to wait for + * @param bWaitAll Specifies whether all or any of the processes/events + * @param duration Duration in milliseconds + * @param expired Set to true if delay period expired + */ +void CoroutineScheduler::waitForMultipleObjects(CORO_PARAM, int nCount, uint32 *pidList, bool bWaitAll, + uint32 duration, bool *expired) { + if (!pCurrent) + error("Called CoroutineScheduler::waitForMultipleEvents from the main process"); + + CORO_BEGIN_CONTEXT; + uint32 endTime; + bool signalled; + bool pidSignalled; + int i; + PROCESS *pProcess; + EVENT *pEvent; + CORO_END_CONTEXT(_ctx); + + CORO_BEGIN_CODE(_ctx); + + // Signal as waiting + pCurrent->waiting = true; + + _ctx->endTime = (duration == CORO_INFINITE) ? CORO_INFINITE : g_system->getMillis() + duration; + if (expired) + // Presume that delay will expire + *expired = true; + + // Outer loop for doing checks until expiry + while (g_system->getMillis() < _ctx->endTime) { + _ctx->signalled = bWaitAll; + + for (_ctx->i = 0; _ctx->i < nCount; ++_ctx->i) { + _ctx->pProcess = getProcess(pidList[_ctx->i]); + _ctx->pEvent = !_ctx->pProcess ? getEvent(pidList[_ctx->i]) : NULL; + + // Determine the signalled state + _ctx->pidSignalled = (_ctx->pProcess) || !_ctx->pEvent ? false : _ctx->pEvent->signalled; + + if (bWaitAll && _ctx->pidSignalled) + _ctx->signalled = false; + else if (!bWaitAll & _ctx->pidSignalled) + _ctx->signalled = true; + } + + // At this point, if the signalled variable is set, waiting is finished + if (_ctx->signalled) { + // Automatically reset any events not flagged for manual reset + for (_ctx->i = 0; _ctx->i < nCount; ++_ctx->i) { + _ctx->pEvent = getEvent(pidList[_ctx->i]); + + if (_ctx->pEvent->manualReset) + _ctx->pEvent->signalled = false; + } + + if (expired) + *expired = false; + break; + } + + // Sleep until the next cycle + CORO_SLEEP(1); + } + + // Signal waiting is done + pCurrent->waiting = false; + + CORO_END_CODE; +} + +/** + * Make the active process sleep for the given duration in milliseconds + * @param duration Duration in milliseconds + * @remarks This duration won't be precise, since it relies on the frequency the + * scheduler is called. + */ +void CoroutineScheduler::sleep(CORO_PARAM, uint32 duration) { + if (!pCurrent) + error("Called CoroutineScheduler::waitForSingleObject from the main process"); + + CORO_BEGIN_CONTEXT; + uint32 endTime; + PROCESS *pProcess; + EVENT *pEvent; + CORO_END_CONTEXT(_ctx); + + CORO_BEGIN_CODE(_ctx); + + // Signal as waiting + pCurrent->waiting = true; + + _ctx->endTime = g_system->getMillis() + duration; + + // Outer loop for doing checks until expiry + while (g_system->getMillis() < _ctx->endTime) { + // Sleep until the next cycle + CORO_SLEEP(1); + } + + // Signal waiting is done + pCurrent->waiting = false; + + CORO_END_CODE; +} + +/** + * Creates a new process. + * + * @param pid process identifier + * @param CORO_ADDR coroutine start address + * @param pParam process specific info + * @param sizeParam size of process specific info + */ +PROCESS *CoroutineScheduler::createProcess(uint32 pid, CORO_ADDR coroAddr, const void *pParam, int sizeParam) { + PROCESS *pProc; + + // get a free process + pProc = pFreeProcesses; + + // trap no free process + assert(pProc != NULL); // Out of processes + +#ifdef DEBUG + // one more process in use + if (++numProcs > maxProcs) + maxProcs = numProcs; +#endif + + // get link to next free process + pFreeProcesses = pProc->pNext; + if (pFreeProcesses) + pFreeProcesses->pPrevious = NULL; + + if (pCurrent != NULL) { + // place new process before the next active process + pProc->pNext = pCurrent->pNext; + if (pProc->pNext) + pProc->pNext->pPrevious = pProc; + + // make this new process the next active process + pCurrent->pNext = pProc; + pProc->pPrevious = pCurrent; + + } else { // no active processes, place process at head of list + pProc->pNext = active->pNext; + pProc->pPrevious = active; + + if (pProc->pNext) + pProc->pNext->pPrevious = pProc; + active->pNext = pProc; + + } + + // set coroutine entry point + pProc->coroAddr = coroAddr; + + // clear coroutine state + pProc->state = 0; + + // wake process up as soon as possible + pProc->sleepTime = 1; + + // set new process id + pProc->pid = pid; + + // set new process specific info + if (sizeParam) { + assert(sizeParam > 0 && sizeParam <= CORO_PARAM_SIZE); + + // set new process specific info + memcpy(pProc->param, pParam, sizeParam); + } + + // return created process + return pProc; +} + +/** + * Creates a new process with an auto-incrementing Process Id. + * + * @param CORO_ADDR coroutine start address + * @param pParam process specific info + * @param sizeParam size of process specific info + */ +uint32 CoroutineScheduler::createProcess(CORO_ADDR coroAddr, const void *pParam, int sizeParam) { + PROCESS *pProc = createProcess(++pidCounter, coroAddr, pParam, sizeParam); + return pProc->pid; +} + +/** + * Creates a new process with an auto-incrementing Process Id, and a single pointer parameter. + * + * @param CORO_ADDR coroutine start address + * @param pParam process specific info + * @param sizeParam size of process specific info + */ +uint32 CoroutineScheduler::createProcess(CORO_ADDR coroAddr, const void *pParam) { + return createProcess(coroAddr, &pParam, sizeof(void *)); +} + + +/** + * Kills the specified process. + * + * @param pKillProc which process to kill + */ +void CoroutineScheduler::killProcess(PROCESS *pKillProc) { + // make sure a valid process pointer + assert(pKillProc >= processList && pKillProc <= processList + CORO_NUM_PROCESS - 1); + + // can not kill the current process using killProcess ! + assert(pCurrent != pKillProc); + +#ifdef DEBUG + // one less process in use + --numProcs; + assert(numProcs >= 0); +#endif + + // Free process' resources + if (pRCfunction != NULL) + (pRCfunction)(pKillProc); + + delete pKillProc->state; + pKillProc->state = 0; + + // Take the process out of the active chain list + pKillProc->pPrevious->pNext = pKillProc->pNext; + if (pKillProc->pNext) + pKillProc->pNext->pPrevious = pKillProc->pPrevious; + + // link first free process after pProc + pKillProc->pNext = pFreeProcesses; + if (pFreeProcesses) + pKillProc->pNext->pPrevious = pKillProc; + pKillProc->pPrevious = NULL; + + // make pKillProc the first free process + pFreeProcesses = pKillProc; +} + + + +/** + * Returns a pointer to the currently running process. + */ +PROCESS *CoroutineScheduler::getCurrentProcess() { + return pCurrent; +} + +/** + * Returns the process identifier of the specified process. + * + * @param pProc which process + */ +int CoroutineScheduler::getCurrentPID() const { + PROCESS *pProc = pCurrent; + + // make sure a valid process pointer + assert(pProc >= processList && pProc <= processList + CORO_NUM_PROCESS - 1); + + // return processes PID + return pProc->pid; +} + +/** + * Kills any process matching the specified PID. The current + * process cannot be killed. + * + * @param pidKill process identifier of process to kill + * @param pidMask mask to apply to process identifiers before comparison + * @return The number of processes killed is returned. + */ +int CoroutineScheduler::killMatchingProcess(uint32 pidKill, int pidMask) { + int numKilled = 0; + PROCESS *pProc, *pPrev; // process list pointers + + for (pProc = active->pNext, pPrev = active; pProc != NULL; pPrev = pProc, pProc = pProc->pNext) { + if ((pProc->pid & (uint32)pidMask) == pidKill) { + // found a matching process + + // dont kill the current process + if (pProc != pCurrent) { + // kill this process + numKilled++; + + // Free the process' resources + if (pRCfunction != NULL) + (pRCfunction)(pProc); + + delete pProc->state; + pProc->state = 0; + + // make prev point to next to unlink pProc + pPrev->pNext = pProc->pNext; + if (pProc->pNext) + pPrev->pNext->pPrevious = pPrev; + + // link first free process after pProc + pProc->pNext = pFreeProcesses; + pProc->pPrevious = NULL; + pFreeProcesses->pPrevious = pProc; + + // make pProc the first free process + pFreeProcesses = pProc; + + // set to a process on the active list + pProc = pPrev; + } + } + } + +#ifdef DEBUG + // adjust process in use + numProcs -= numKilled; + assert(numProcs >= 0); +#endif + + // return number of processes killed + return numKilled; +} + +/** + * Set pointer to a function to be called by killProcess(). + * + * May be called by a resource allocator, the function supplied is + * called by killProcess() to allow the resource allocator to free + * resources allocated to the dying process. + * + * @param pFunc Function to be called by killProcess() + */ +void CoroutineScheduler::setResourceCallback(VFPTRPP pFunc) { + pRCfunction = pFunc; +} + +PROCESS *CoroutineScheduler::getProcess(uint32 pid) { + PROCESS *pProc = active->pNext; + while ((pProc != NULL) && (pProc->pid != pid)) + pProc = pProc->pNext; + + return pProc; +} + +EVENT *CoroutineScheduler::getEvent(uint32 pid) { + Common::List<EVENT *>::iterator i; + for (i = _events.begin(); i != _events.end(); ++i) { + EVENT *evt = *i; + if (evt->pid == pid) + return evt; + } + + return NULL; +} + + +/** + * Creates a new event object + * @param bManualReset Events needs to be manually reset. Otherwise, events + * will be automatically reset after a process waits on the event finishes + * @param bInitialState Specifies whether the event is signalled or not initially + */ +uint32 CoroutineScheduler::createEvent(bool bManualReset, bool bInitialState) { + EVENT *evt = new EVENT(); + evt->pid = ++pidCounter; + evt->manualReset = bManualReset; + evt->signalled = bInitialState; + + _events.push_back(evt); + return evt->pid; +} + +/** + * Destroys the given event + * @param pidEvent Event PID + */ +void CoroutineScheduler::closeEvent(uint32 pidEvent) { + EVENT *evt = getEvent(pidEvent); + if (evt) { + _events.remove(evt); + delete evt; + } +} + +/** + * Sets the event + * @param pidEvent Event PID + */ +void CoroutineScheduler::setEvent(uint32 pidEvent) { + EVENT *evt = getEvent(pidEvent); + if (evt) + evt->signalled = true; +} + +/** + * Resets the event + * @param pidEvent Event PID + */ +void CoroutineScheduler::resetEvent(uint32 pidEvent) { + EVENT *evt = getEvent(pidEvent); + if (evt) + evt->signalled = false; +} + +/** + * Temporarily sets a given event to true, and then runs all waiting processes, allowing any + * processes waiting on the event to be fired. It then immediately resets the event again. + * @param pidEvent Event PID + * + * @remarks Should not be run inside of another process + */ +void CoroutineScheduler::pulseEvent(uint32 pidEvent) { + EVENT *evt = getEvent(pidEvent); + if (!evt) + return; + + // Set the event as true + evt->signalled = true; + + // start dispatching active process list for any processes that are currently waiting + PROCESS *pOriginal = pCurrent; + PROCESS *pNext; + PROCESS *pProc = active->pNext; + while (pProc != NULL) { + pNext = pProc->pNext; + + // Only call processes that are currently waiting (either in waitForSingleObject or + // waitForMultipleObjects). If one is found, execute it immediately + if (pProc->waiting) { + // Dispatch the process + pCurrent = pProc; + pProc->coroAddr(pProc->state, pProc->param); + + if (!pProc->state || pProc->state->_sleep <= 0) { + // Coroutine finished + pCurrent = pCurrent->pPrevious; + killProcess(pProc); + } else { + pProc->sleepTime = pProc->state->_sleep; + } + + // pCurrent may have been changed + pNext = pCurrent->pNext; + pCurrent = NULL; + } + + pProc = pNext; + } + + // Restore the original current process (if one was active) + pCurrent = pOriginal; + + // Reset the event back to non-signalled + evt->signalled = false; +} + + +} // end of namespace Common diff --git a/common/coroutines.h b/common/coroutines.h new file mode 100644 index 0000000000..3303028e1c --- /dev/null +++ b/common/coroutines.h @@ -0,0 +1,398 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program 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. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef COMMON_COROUTINES_H +#define COMMON_COROUTINES_H + +#include "common/scummsys.h" +#include "common/util.h" // for SCUMMVM_CURRENT_FUNCTION +#include "common/list.h" +#include "common/singleton.h" + +namespace Common { + +/** + * @defgroup Coroutine support for simulating multi-threading. + * + * The following is loosely based on an article by Simon Tatham: + * <http://www.chiark.greenend.org.uk/~sgtatham/coroutines.html>. + * However, many improvements and tweaks have been made, in particular + * by taking advantage of C++ features not available in C. + */ +//@{ + +#define CoroScheduler (Common::CoroutineScheduler::instance()) + + +// Enable this macro to enable some debugging support in the coroutine code. +//#define COROUTINE_DEBUG 1 + +/** + * The core of any coroutine context which captures the 'state' of a coroutine. + * Private use only. + */ +struct CoroBaseContext { + int _line; + int _sleep; + CoroBaseContext *_subctx; +#if COROUTINE_DEBUG + const char *_funcName; +#endif + CoroBaseContext(const char *func); + ~CoroBaseContext(); +}; + +typedef CoroBaseContext *CoroContext; + + +/** This is a special constant that can be temporarily used as a parameter to call coroutine-ised + * from methods from methods that haven't yet been converted to being a coroutine, so code at least + * compiles correctly. Be aware, though, that if you use this, you will get runtime errors. + */ +extern CoroContext nullContext; + +/** + * Wrapper class which holds a pointer to a pointer to a CoroBaseContext. + * The interesting part is the destructor, which kills the context being held, + * but ONLY if the _sleep val of that context is zero. This way, a coroutine + * can just 'return' w/o having to worry about freeing the allocated context + * (in Simon Tatham's original code, one had to use a special macro to + * return from a coroutine). + */ +class CoroContextHolder { + CoroContext &_ctx; +public: + CoroContextHolder(CoroContext &ctx) : _ctx(ctx) { + assert(ctx); + assert(ctx->_sleep >= 0); + ctx->_sleep = 0; + } + ~CoroContextHolder() { + if (_ctx && _ctx->_sleep == 0) { + delete _ctx; + _ctx = 0; + } + } +}; + +/** Methods that have been converted to being a coroutine should have this as the first parameter */ +#define CORO_PARAM Common::CoroContext &coroParam + + +/** + * Begin the declaration of a coroutine context. + * This allows declaring variables which are 'persistent' during the + * lifetime of the coroutine. An example use would be: + * + * CORO_BEGIN_CONTEXT; + * int var; + * char *foo; + * CORO_END_CONTEXT(_ctx); + * + * It is not possible to initialize variables here, due to the way this + * macro is implemented. Furthermore, to use the variables declared in + * the coroutine context, you have to access them via the context variable + * name that was specified as parameter to CORO_END_CONTEXT, e.g. + * _ctx->var = 0; + * + * @see CORO_END_CONTEXT + * + * @note We declare a variable 'DUMMY' to allow the user to specify an 'empty' + * context, and so compilers won't complain about ";" following the macro. + */ +#define CORO_BEGIN_CONTEXT \ + struct CoroContextTag : Common::CoroBaseContext { \ + CoroContextTag() : CoroBaseContext(SCUMMVM_CURRENT_FUNCTION) {} \ + int DUMMY + +/** + * End the declaration of a coroutine context. + * @param x name of the coroutine context + * @see CORO_BEGIN_CONTEXT + */ +#define CORO_END_CONTEXT(x) } *x = (CoroContextTag *)coroParam + +/** + * Begin the code section of a coroutine. + * @param x name of the coroutine context + * @see CORO_BEGIN_CODE + */ +#define CORO_BEGIN_CODE(x) \ + if (&coroParam == &Common::nullContext) assert(!Common::nullContext);\ + if (!x) {coroParam = x = new CoroContextTag();}\ + Common::CoroContextHolder tmpHolder(coroParam);\ + switch (coroParam->_line) { case 0:; + +/** + * End the code section of a coroutine. + * @see CORO_END_CODE + */ +#define CORO_END_CODE \ + if (&coroParam == &Common::nullContext) { \ + delete Common::nullContext; \ + Common::nullContext = NULL; \ + } \ + } + +/** + * Sleep for the specified number of scheduler cycles. + */ +#define CORO_SLEEP(delay) do {\ + coroParam->_line = __LINE__;\ + coroParam->_sleep = delay;\ + assert(&coroParam != &Common::nullContext);\ + return; case __LINE__:;\ + } while (0) + +#define CORO_GIVE_WAY do { CoroScheduler.giveWay(); CORO_SLEEP(1); } while (0) +#define CORO_RESCHEDULE do { CoroScheduler.reschedule(); CORO_SLEEP(1); } while (0) + +/** + * Stop the currently running coroutine and all calling coroutines. + * + * This sets _sleep to -1 rather than 0 so that the context doesn't get + * deleted by CoroContextHolder, since we want CORO_INVOKE_ARGS to + * propogate the _sleep value and return immediately (the scheduler will + * then delete the entire coroutine's state, including all subcontexts). + */ +#define CORO_KILL_SELF() \ + do { if (&coroParam != &Common::nullContext) { coroParam->_sleep = -1; } return; } while (0) + + +/** + * This macro is to be used in conjunction with CORO_INVOKE_ARGS and + * similar macros for calling coroutines-enabled subroutines. + */ +#define CORO_SUBCTX coroParam->_subctx + +/** + * Invoke another coroutine. + * + * If the subcontext still exists after the coroutine is invoked, it has + * either yielded/slept or killed itself, and so we copy the _sleep value + * to our own context and return (execution will continue at the case + * statement below, where we loop and call the coroutine again). + * If the subcontext is null, the coroutine ended normally, and we can + * simply break out of the loop and continue execution. + * + * @param subCoro name of the coroutine-enabled function to invoke + * @param ARGS list of arguments to pass to subCoro + * + * @note ARGS must be surrounded by parentheses, and the first argument + * in this list must always be CORO_SUBCTX. For example, the + * regular function call + * myFunc(a, b); + * becomes the following: + * CORO_INVOKE_ARGS(myFunc, (CORO_SUBCTX, a, b)); + */ +#define CORO_INVOKE_ARGS(subCoro, ARGS) \ + do {\ + coroParam->_line = __LINE__;\ + coroParam->_subctx = 0;\ + do {\ + subCoro ARGS;\ + if (!coroParam->_subctx) break;\ + coroParam->_sleep = coroParam->_subctx->_sleep;\ + assert(&coroParam != &Common::nullContext);\ + return; case __LINE__:;\ + } while (1);\ + } while (0) + +/** + * Invoke another coroutine. Similar to CORO_INVOKE_ARGS, + * but allows specifying a return value which is returned + * if invoked coroutine yields (thus causing the current + * coroutine to yield, too). + */ +#define CORO_INVOKE_ARGS_V(subCoro, RESULT, ARGS) \ + do {\ + coroParam->_line = __LINE__;\ + coroParam->_subctx = 0;\ + do {\ + subCoro ARGS;\ + if (!coroParam->_subctx) break;\ + coroParam->_sleep = coroParam->_subctx->_sleep;\ + assert(&coroParam != &Common::nullContext);\ + return RESULT; case __LINE__:;\ + } while (1);\ + } while (0) + +/** + * Convenience wrapper for CORO_INVOKE_ARGS for invoking a coroutine + * with no parameters. + */ +#define CORO_INVOKE_0(subCoroutine) \ + CORO_INVOKE_ARGS(subCoroutine,(CORO_SUBCTX)) + +/** + * Convenience wrapper for CORO_INVOKE_ARGS for invoking a coroutine + * with one parameter. + */ +#define CORO_INVOKE_1(subCoroutine, a0) \ + CORO_INVOKE_ARGS(subCoroutine,(CORO_SUBCTX,a0)) + +/** + * Convenience wrapper for CORO_INVOKE_ARGS for invoking a coroutine + * with two parameters. + */ +#define CORO_INVOKE_2(subCoroutine, a0,a1) \ + CORO_INVOKE_ARGS(subCoroutine,(CORO_SUBCTX,a0,a1)) + +/** + * Convenience wrapper for CORO_INVOKE_ARGS for invoking a coroutine + * with three parameters. + */ +#define CORO_INVOKE_3(subCoroutine, a0,a1,a2) \ + CORO_INVOKE_ARGS(subCoroutine,(CORO_SUBCTX,a0,a1,a2)) + +/** + * Convenience wrapper for CORO_INVOKE_ARGS for invoking a coroutine + * with four parameters. + */ +#define CORO_INVOKE_4(subCoroutine, a0,a1,a2,a3) \ + CORO_INVOKE_ARGS(subCoroutine,(CORO_SUBCTX,a0,a1,a2,a3)) + + + +// the size of process specific info +#define CORO_PARAM_SIZE 32 + +// the maximum number of processes +#define CORO_NUM_PROCESS 100 +#define CORO_MAX_PROCESSES 100 + +#define CORO_INFINITE 0xffffffff +#define CORO_INVALID_PID_VALUE 0 + +typedef void (*CORO_ADDR)(CoroContext &, const void *); + +/** process structure */ +struct PROCESS { + PROCESS *pNext; ///< pointer to next process in active or free list + PROCESS *pPrevious; ///< pointer to previous process in active or free list + + CoroContext state; ///< the state of the coroutine + CORO_ADDR coroAddr; ///< the entry point of the coroutine + + int sleepTime; ///< number of scheduler cycles to sleep + uint32 pid; ///< process ID + bool waiting; ///< process is currently in a waiting state + char param[CORO_PARAM_SIZE]; ///< process specific info +}; +typedef PROCESS *PPROCESS; + + +/** Event structure */ +struct EVENT { + uint32 pid; + bool manualReset; + bool signalled; +}; + + +/** + * Creates and manages "processes" (really coroutines). + */ +class CoroutineScheduler: public Singleton<CoroutineScheduler> { +public: + /** Pointer to a function of the form "void function(PPROCESS)" */ + typedef void (*VFPTRPP)(PROCESS *); + +private: + + /** list of all processes */ + PROCESS *processList; + + /** active process list - also saves scheduler state */ + PROCESS *active; + + /** pointer to free process list */ + PROCESS *pFreeProcesses; + + /** the currently active process */ + PROCESS *pCurrent; + + /** Auto-incrementing process Id */ + int pidCounter; + + /** Event list */ + Common::List<EVENT *> _events; + +#ifdef DEBUG + // diagnostic process counters + int numProcs; + int maxProcs; + + void CheckStack(); +#endif + + /** + * Called from killProcess() to enable other resources + * a process may be allocated to be released. + */ + VFPTRPP pRCfunction; + + PROCESS *getProcess(uint32 pid); + EVENT *getEvent(uint32 pid); +public: + + CoroutineScheduler(); + ~CoroutineScheduler(); + + void reset(); + + #ifdef DEBUG + void printStats(); + #endif + + void schedule(); + void rescheduleAll(); + void reschedule(PPROCESS pReSchedProc = NULL); + void giveWay(PPROCESS pReSchedProc = NULL); + void waitForSingleObject(CORO_PARAM, int pid, uint32 duration, bool *expired = NULL); + void waitForMultipleObjects(CORO_PARAM, int nCount, uint32 *pidList, bool bWaitAll, + uint32 duration, bool *expired = NULL); + void sleep(CORO_PARAM, uint32 duration); + + PROCESS *createProcess(uint32 pid, CORO_ADDR coroAddr, const void *pParam, int sizeParam); + uint32 createProcess(CORO_ADDR coroAddr, const void *pParam, int sizeParam); + uint32 createProcess(CORO_ADDR coroAddr, const void *pParam); + void killProcess(PROCESS *pKillProc); + + PROCESS *getCurrentProcess(); + int getCurrentPID() const; + int killMatchingProcess(uint32 pidKill, int pidMask = -1); + + void setResourceCallback(VFPTRPP pFunc); + + /* Event methods */ + uint32 createEvent(bool bManualReset, bool bInitialState); + void closeEvent(uint32 pidEvent); + void setEvent(uint32 pidEvent); + void resetEvent(uint32 pidEvent); + void pulseEvent(uint32 pidEvent); +}; + +//@} + +} // end of namespace Common + +#endif // COMMON_COROUTINES_H diff --git a/common/module.mk b/common/module.mk index 7e31ddfa01..92279740e5 100644 --- a/common/module.mk +++ b/common/module.mk @@ -4,6 +4,7 @@ MODULE_OBJS := \ archive.o \ config-file.o \ config-manager.o \ + coroutines.o \ dcl.o \ debug.o \ error.o \ |