|
IdeasSandbox
Miscellany, Pot pourri, Hodgepodge, and even the Kitchen Sink
Phase-Design IntroductionWe the developers will place random thoughts about design and the implementation of the game or game engine here. On Overloading Stream OperatorsThis section describes a proposal for standards of overloading the iostreams stream operators. OverviewA major benefit for any application is being able to quickly and easily input and output data in both human-readable and machine-readable formats. This aids in debugging, interaction, and transparency in a piece of software. Luckily, C++ provides a very extensible framework in its standard library, the iostreams framework, to allow the input and output of data into and out of internal types using a standard interface. The Humm and Strumm Game Engine should take advantage of this framework wherever possible. It must, however, do so in a consistent and standard way. Every type in the engine for which it makes sense needs to interface with the iostreams library. Every type MUST overload the stream operators if it makes sense for it to do so. Human- and Machine-readableThough the iostreams library does not specify how the input and output should be structured, we need to make this decision for consistency. Because our project is an inherently graphical one, there is not much need for user-readability; for debugging purposes, though, an output representation should be human-readable. Furthermore, a machine should be able to read input, preferably in the exact same format that it has outputted. This allows for deterministic run-time diagnostics. The overloaded stream operators for a type MUST input and output in a single format, that both a human and a computer can read. LocalizationNow consider a French system, in which the decimal part of a number is separated from the integral part by the , symbol. A configuration file that was created, for instance, on an American system using a . as the radix point, would create potential ambiguity to the French system. Because different locales specify different characters for important features of input, like the radix point, we must use a standard locale for input and output, where the characters are specified definitely across all systems. The iostreams library defaults to using such a locale, called the C locale. However, we cannot assume that any given stream will be using the C locale when it is passed into the stream operator function. As such, each stream operator function should have the following form: std::ostream &
operator<< (std::ostream &out, Foo foo)
{
// Change the locale to C, saving the stream's current locale.
std::locale c {"C"};
std::locale old {out.imbue (c)};
/* Do something here to output foo.
....
*/
// Restore the old locale of the stream.
out.imbue (old);
return out;
}This will make sure that the C locale is used for reading and writing an object and also that the user's intended locale can still be used on the stream. If it is necessary to write a locale-specific string of a type, a separate method should be used. If outputting the type with a different locale should change its representation (though this is unlikely due to the above requirement), you do not have to change the stream locale to the C locale. The overloaded stream operators for a type SHOULD set imbue the C locale at the start of the function and restore the prior locale at the end of the function. FormatThe standard types supported by the iostreams all have representations that are single tokens: there is no internal whitespace. The only exceptions are const char * and std::string, which can output a full string with spaces, but cannot read one in with the operator>> overload they provided. Because we do not allow different output and input representations (as explained above), our classes should not do this. Keeping with the general practice of the standard library, types should have representations that are only a single token, so they can be delimited by whitespace. This is intuitive to users of our overloaded operator>> and operator<< methods. However, certain representations may require spaces, either to be legible, to be unambiguous, or to keep with standard practices for a data representation format. In these cases, there needs to be some delimiter. A mathematical vector class, for instance, might have an output <a, b, c, d>, where a, b, c and d are floating-point numbers. The overloaded stream operators for a type SHOULD input and output in a single token (with no internal whitespace), unless otherwise delimited. MultithreadingThis section lays out a proposal for the creation of a Task Manager System for the game engine. This system is dependent on some changes in the Heap and in a Messaging System for it to work (outlined elsewhere). Why Do We Need a Task Manager?Traditionally, a game loop has been designed in a very serial way, such that one task (say, updating the game world) always preceded another task (the rendering, for instance). For a single-processor system, this is quite an efficient solution, as it avoids many complexities of programming concurrently and requires no context switching. In a modern system, with multiple cores available, this is terribly inefficient. The game would only take advantage of one core (as it always had). The other cores would be idling, or worse, devoted to background tasks. All this untapped power is available to the system. To take advantage of these new multicore systems, we need to use threads. Threads allow for multiple paths of execution to take place at once, even on a single core system. With a multicore system, however, threads can be evenly distributed on the system's cores. It would be quite easy to just create a thread for each entity in the game, and a few more for the subsystems, for good measure. This presents serious problems in performance, though. With that many threads, the system cannot run them all truly concurrently. It instead has to fake it with context switching. This involves interrupting the currently executing thread, saving its state, switching to another thread, and loading its saved state. When done enough, not only do the threads need to wait to gain control of the processor, but the system needs to wait for the saving and loading of the threads' contexts. The most efficient solution, that which is taken here, is to create a single thread for each core, and then have actual functionality distributed among the threads. With proper data management, this allows for the optimal increase in speed allowed for by a modern multicore system. How Does this Work?There are the same number of threads created as there are cores on the system. Instead of creating new threads, tasks are allocated as jobs for each thread to complete. This minimises context switching between threads on the same core, while fully benefiting from the parallel execution model provided by multicore machines. If destroyed carefully, we will have no need for a master manager thread. All threads (including the main() thread) can then be utilised. For use in a game engine, a task manager needs to be able to synchronise its threads, starting now jobs at the same time; to be able to sustain itself without a full manager, allowing it to use all threads for jobs; and to be able to accept new jobs. Synchronise ThreadsWhen a thread completes its current job, it searches for a new one (held in a locked queue). If there are no more open jobs, the thread waits and is said to be synchronising. The final thread to finish its job switches the queue to the system queue which has functions like input polling, message passing, and rendering. The thread then sets its state to synchronising to alert the other threads to continue with the new queue. As before, the threads synchronise, and the queue is switched back to the object queue. The task manager is now said to have taken a step. Sustain ThreadsThe task manager itself conceptually is nothing more than a container for the two job queues and an initialiser for the threads of the other cores. There is no true master thread that controls the others. Instead. threads delegate management functions based on state, such that every thread is able to perform these functions, but only one actually does. The task manager first spawns enough threads to execute on the other cores. For examples, on a four core system, three more threads are spawned. Then all threads begin executing jobs. The jobs are stored in the two queues--the object queue, which contains jobs specific to the game and certain jobs from the game engine which require the messaging system, and the system queue, which contains update jobs that need information from the objects to execute--and are distributed to each thread on a first-come, first-served basis. Each step the jobs of the object queue are performed, the threads synchronised, and the jobs of the system queue are performed. Job ManagementTODO: Add.
Copyright (C) 2008-2012, the people listed in the Authors page. This page is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License. | |