changeset 85:56c0ddd90193

Intermediate commit (not stable). Changes to init system.
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 11 Sep 2008 11:33:51 +0100
parents e0f1ec7fe73a
children 79d816b3e2d2
files codeDoc/debugCodes.txt codeDoc/ideas.txt codeDoc/jobs.txt codeDoc/staticCtors.txt data/conf/options.mtt doc/Readme.txt dsss.conf examples/guiDemo.d mde/events.d mde/file/mergetag/Reader.d mde/file/mergetag/Writer.d mde/file/mergetag/iface/IReader.d mde/font/FontTexture.d mde/font/font.d mde/gl/draw.d mde/gui/WidgetManager.d mde/gui/widget/Ifaces.d mde/gui/widget/createWidget.d mde/gui/widget/layout.d mde/input/Config.d mde/input/joystick.d mde/lookup/Options.d mde/mde.d mde/scheduler/Scheduler.d mde/setup/Init.d mde/setup/InitStage.d mde/setup/Screen.d mde/setup/exception.d mde/setup/init2.d mde/setup/initFunctions.d mde/setup/paths.d mde/setup/sdl.d
diffstat 32 files changed, 930 insertions(+), 757 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/codeDoc/debugCodes.txt	Thu Sep 11 11:33:51 2008 +0100
@@ -0,0 +1,15 @@
+Copyright © 2007-2008 Diggory Hardy
+License: GNU General Public License version 2 or later (see COPYING)
+
+
+General use of debug statement:
+Non-critical assertions (i.e. for bug-tracking, usually for conditions which _should_ be guaranteed by the code).
+Logging of all trace messages.
+
+
+Debug codes currently used in mde:
+
+- code -                - purpose -
+mdeUnitTest             Compile in unittest code (including extra imports, and a few other bits outside the unittest itself).
+drawGlyphCache          Draw the font texture in the upper-left corner of the screen, with a pretty background.
+mdeWidgets              Log trace messages for the creation of all widgets.
--- a/codeDoc/ideas.txt	Sun Aug 31 15:59:17 2008 +0100
+++ b/codeDoc/ideas.txt	Thu Sep 11 11:33:51 2008 +0100
@@ -1,3 +1,7 @@
+Copyright © 2007-2008 Diggory Hardy
+License: GNU General Public License version 2 or later (see COPYING)
+
+
 Miscelaneous ideas for mde.
 
 Make a special "trace" logger which keeps the last 20, say, trace messages and only output them when asked to do so, which might happen when an exception is caught. It might output them via the normal mechanisms, but only when asked (and not all messages may be in the correct order: trace messages might be logged later than they were added to the list).
--- a/codeDoc/jobs.txt	Sun Aug 31 15:59:17 2008 +0100
+++ b/codeDoc/jobs.txt	Thu Sep 11 11:33:51 2008 +0100
@@ -3,12 +3,18 @@
 
 
 In progress:
+why is nothing drawn until a resize? seems to be working now (??)
+why isn't font texture drawn in mde?
+why does mde&guiDemo hang on exit, only if no resize occurred?
+wierd things with options.mtt (font section getting output twice)
+CircularIterator
+unittest again!
 
 
 
 To do (importance 0-5: 0 pointless, 1 no obvious impact now, 2 todo sometime, 3 useful, 4 important, 5 urgent):
 Also see todo.txt and FIXME/NOTE comment marks.
-5   setting widgets' default size? setMinSize/setDefaultSize fct?
+4   Why does mde.events need to be imported before mde.setup.Init to make fonts display properly? Analysis of static CTORs doesn't seem to have helped. Could be to do with order init functions are run in? Crash in threaded mode.
 4   Try to correlate names of option sections more. (i.e. symbol name, class name, name of i18n translation file)
 4   Not guaranteed to catch up-click ending callback! Appears not to be a problem...
 4   OutOfMemoryException is not currently checked for − it should be at least in critical places (use high-level catching of all errors?).
@@ -18,7 +24,6 @@
 3   Scheduler for drawing only windows which need redrawing.
 3   Update scheduler as outlined in FIXME.
 3   Windows building/compatibility (currently partial) - tango/sys/win32/SpecialPath.d
-2   Why does mde.events need to be imported before mde.setup.Init to make fonts display properly? Analysis of static CTORs doesn't seem to have helped.
 2   Remove ability to scan, then load, mergetag sections. Not so necessary with section creator callback and allows "sliding window" type partial buffering.
 2   Options need a "level": simple options, for advanced users, for debugging only, etc.
 2   Command-line options for paths to by-pass normal path finding functionality.
--- a/codeDoc/staticCtors.txt	Sun Aug 31 15:59:17 2008 +0100
+++ b/codeDoc/staticCtors.txt	Thu Sep 11 11:33:51 2008 +0100
@@ -1,4 +1,8 @@
-Map of what happens in static CTORs (excluding creating loggers):
+Copyright © 2007-2008 Diggory Hardy
+License: GNU General Public License version 2 or later (see COPYING)
+
+
+Map of what happens in static CTORs (excluding creating loggers). From August 2008.
 
 mde
 ->  imde
--- a/data/conf/options.mtt	Sun Aug 31 15:59:17 2008 +0100
+++ b/data/conf/options.mtt	Thu Sep 11 11:33:51 2008 +0100
@@ -1,9 +1,9 @@
 {MT01}
 {misc}
-<bool|useThreads=false>
+<int|numThreads=2>
 <bool|exitImmediately=false>
 <char[]|L10n="en-GB">
-<int|logOptions=0x3001>
+<int|logOptions=0x3000>
 <double|pollInterval=0.01>
 
 {font}
--- a/doc/Readme.txt	Sun Aug 31 15:59:17 2008 +0100
+++ b/doc/Readme.txt	Thu Sep 11 11:33:51 2008 +0100
@@ -33,7 +33,7 @@
 
 Also thanks to:
 Walter Bright and Digital Mars for D and DMD.
-The tango team for Tango.
+The tango team.
 [derelict]
 [sdl]
 [opengl]
@@ -59,18 +59,14 @@
 
 + How many lines are there?
 # wc -l $(find . -name "*.d")
-As of last count, that was 3908.
+As of my last count, that was 10227.
 
 + Why is the code so DENSE?
 That's just my coding style (I still mostly stick to the D style guide). I like code to look neat, but I don't like spacing it out a lot because then it takes more room (more scrolling) and IMO it becomes harder to visualise what goes on.
 
 + What toolkits/external libraries are used?
     tango
-    tango.scrapple
-    SDL (possibly will be replaced)
+    derelict (SDL, OpenGL, FreeType 2)
 
 + What libraries are planned to be used?
-    OpenGL (of course...)
-    OpenAL (most likely; however audio support is a LONG way off)
-    Schooner: GLD & fonts (or conversions to tango) ?
-    GLFW ?
+    OpenAL, if I get around to doing anything with audio.
--- a/dsss.conf	Sun Aug 31 15:59:17 2008 +0100
+++ b/dsss.conf	Thu Sep 11 11:33:51 2008 +0100
@@ -1,7 +1,7 @@
 # Copyright © 2007-2008 Diggory Hardy
 # License: GNU General Public License version 2 or later (see COPYING)
 
-defaulttargets = mde/mde.d
+defaulttargets = mde/mde.d examples/guiDemo.d
 
 [*]
 version (Posix) {
@@ -14,5 +14,8 @@
 [mde/mde.d]
 target=bin/mde
 
+[examples/guiDemo.d]
+target=bin/guiDemo
+
 [util/mtcp.d]
 target=bin/mtcp
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/guiDemo.d	Thu Sep 11 11:33:51 2008 +0100
@@ -0,0 +1,81 @@
+/* LICENSE BLOCK
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+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, see <http://www.gnu.org/licenses/>. */
+
+/** Main module for a gui demo & testing executable. */
+module examples.guiDemo;
+
+import mde.imde;                        // this module's interface for external modules
+import mde.events;      // pollEvents() // NOTE: Must be imported before Init, otherwise fonts don't display properly (why??)
+import mde.setup.Init;                  // initialization
+import mde.lookup.Options;              // pollInterval option
+import mde.scheduler.Scheduler;         // mainSchedule
+import mde.setup.Screen;                // Screen.draw()
+import mde.setup.InitStage;             // StageState
+import mde.gui.WidgetManager;
+
+import tango.core.Thread : Thread;	// Thread.sleep()
+import tango.time.Clock;                // Clock.now()
+import tango.util.log.Log : Log, Logger;
+debug (mdeUnitTest) {
+    import mde.file.ssi;
+    import mde.file.mergetag.mdeUT;
+}
+
+int main(char[][] args)
+{
+    Logger logger = Log.getLogger ("mde.mde");
+    // If compiled with unittests, notify that they completed and exit:
+    debug (mdeUnitTest) {
+        logger.info ("Compiled unittests have completed; terminating.");
+        return 0;
+    }
+    
+    // Set up the gui
+    scope WidgetManager gui = new WidgetManager ("gui");
+    StageState guiLoad () {   // init func
+        gui.init;
+        gui.loadDesign();
+        return StageState.ACTIVE;
+    }
+    StageState guiSave () {
+        gui.save;
+        return StageState.INACTIVE;
+    }
+    addInitStage ("GuiM", &guiLoad, &guiSave, ["SWnd", "Font"]);
+    
+    scope Init init = new Init(args);	// initialize mde
+    
+    // Make sure pollInterval has a sane value. FIXME: get Options class to enforce range
+    if (miscOpts.pollInterval !<= 1.0 || miscOpts.pollInterval !>= 0.0)
+        miscOpts.set!(double) ("pollInterval", 0.01);
+    
+    //BEGIN Main loop setup
+    /* Note: the main loop is currently controlled by the scheduler. This is not really ideal,
+     * since it provides no direct control of the order in which components are executed and does
+     * not allow running components simultaeneously with threads.
+     * Note: probably drawing should start at the beginning of the loop and glFlush()/swapBuffers
+     * be called at the end to optimise. */
+    mainSchedule.add (SCHEDULE.DRAW, &Screen.draw); // Draw, per event only.
+    mainSchedule.add (mainSchedule.getNewID, &mde.events.pollEvents).frame = true;
+    //END Main loop setup
+    
+    while (run) {
+        mainSchedule.execute (Clock.now());
+        
+        Thread.sleep (miscOpts.pollInterval);	// sleep this many seconds
+    }
+    
+    return 0;		// cleanup handled by init's DTOR
+}
--- a/mde/events.d	Sun Aug 31 15:59:17 2008 +0100
+++ b/mde/events.d	Thu Sep 11 11:33:51 2008 +0100
@@ -20,7 +20,7 @@
 module mde.events;
 
 import imde = mde.imde;
-import sdl = mde.setup.sdl;	// resizeWindow
+import mde.setup.Screen;
 
 import mde.input.Input;
 
@@ -44,7 +44,7 @@
                 imde.run = false;
                 break;
             case SDL_VIDEORESIZE:
-                sdl.resizeWindow (event.resize.w, event.resize.h);
+                Screen.resizeEvent (event.resize.w, event.resize.h);
                 imde.mainSchedule.request(imde.SCHEDULE.DRAW);
                 break;
             case SDL_ACTIVEEVENT:
--- a/mde/file/mergetag/Reader.d	Sun Aug 31 15:59:17 2008 +0100
+++ b/mde/file/mergetag/Reader.d	Thu Sep 11 11:33:51 2008 +0100
@@ -32,8 +32,7 @@
 import tango.io.UnicodeFile;
 import Util = tango.text.Util;
 import ConvInt = tango.text.convert.Integer;
-//import tango.util.collection.model.View : View;
-import tango.util.collection.HashSet : HashSet;
+import tango.util.container.HashSet;
 import tango.util.log.Log : Log, Logger;
 
 private Logger logger;
@@ -305,7 +304,7 @@
         read (hs);
     }
     /** ditto */
-    public void read (View!(ID) secSet) {
+    public void read (IContainer!(ID) secSet) {
         if (allRead || fatal) return;			// never do anything in either case
         
         if (secTable.length) {
@@ -516,5 +515,5 @@
     }
     void read () {}                     /// Commence reading
     void read (ID[] secSet) {}          /// ditto
-    void read (View!(ID) secSet) {}     /// ditto
+    void read (IContainer!(ID) secSet) {}/// ditto
 }
--- a/mde/file/mergetag/Writer.d	Sun Aug 31 15:59:17 2008 +0100
+++ b/mde/file/mergetag/Writer.d	Thu Sep 11 11:33:51 2008 +0100
@@ -36,7 +36,7 @@
 
 // tango imports
 import tango.core.Exception;
-import tango.io.FileConduit;
+import tango.io.device.FileConduit;
 import tango.io.Buffer : Buffer, IBuffer;
 import tango.io.Print : Print;
 import convInt = tango.text.convert.Integer;
@@ -162,6 +162,8 @@
     public this (char[] path, DataSet ds = null) {
         _path = path;
         _dataset = ds;
+        foreach (i,s; _dataset.sec)
+            debug logger.trace ("sec ID length: {}", i.length);
     }
 //END CTOR / DTOR
     
@@ -178,28 +180,38 @@
         if (!_dataset) throwMTErr ("write(): no Dataset available to write from!", new MTNoDataSetException ());
         
         try {
+            debug logger.trace ("W.w: 1");
             FileConduit conduit;	// actual conduit; don't use directly when there's content in the buffer
             IBuffer buffer;		// write strings directly to this (use opCall(void[]) )
             
+            debug logger.trace ("W.w: 2");
             // Open a conduit on the file:
             conduit = new FileConduit (_path, FileConduit.WriteCreate);
             scope(exit) conduit.close();
             
+            debug logger.trace ("W.w: 3");
             buffer = new Buffer(conduit);	// And a buffer
             scope(exit) buffer.flush();
             
+            debug logger.trace ("W.w: 4");
             // Write the header:
             buffer ("{MT" ~ MTFormatVersion.CurrentString ~ "}" ~ Eol);
+            debug logger.trace ("W.w: 5");
             if (_dataset.header !is null) writeSection (buffer, _dataset.header);
         
+            debug logger.trace ("W.w: 6");
             // Write the rest:
             foreach (ID id, IDataSection sec; _dataset.sec) {
+                debug logger.trace ("W.w: 71");
                 writeSectionIdentifier (buffer, id);
+                debug logger.trace ("W.w: 72");
                 writeSection (buffer, sec);
             }
-        
+            
+            debug logger.trace ("W.w: 8");
             buffer.flush();
             
+            debug logger.trace ("W.w: 9");
         }
         catch (IOException e) {
             throwMTErr ("Error writing to file: " ~ e.msg, new MTFileIOException);
@@ -210,7 +222,14 @@
     }
         
     private void writeSectionIdentifier (IBuffer buffer, ID id) {
-        buffer ("{" ~ cast(char[])id ~ "}" ~ Eol);
+        debug logger.trace ("W.wSI: 0");
+        debug logger.trace ("W.wSI: id ({})",id.length);
+        debug logger.trace ("W.wSI: Eol ({})",Eol.length);
+        char[] tp = "{" ~ cast(char[])id ~ "}" ~ Eol;
+        debug logger.trace ("W.wSI: 0.1");
+        debug logger.trace ("W.wSI: string ({}): {}",tp.length, tp);
+        buffer (tp);
+        debug logger.trace ("W.wSI: 1");
     }
     
     private void writeSection (IBuffer buffer, IDataSection sec) {
--- a/mde/file/mergetag/iface/IReader.d	Sun Aug 31 15:59:17 2008 +0100
+++ b/mde/file/mergetag/iface/IReader.d	Thu Sep 11 11:33:51 2008 +0100
@@ -20,7 +20,7 @@
 
 import mde.file.mergetag.DataSet;
 
-import tango.util.collection.model.View : View;
+public import tango.util.container.model.IContainer;
 
 /** Interface for all mergetag readers (MTTReader etc.).
 */
@@ -33,5 +33,5 @@
     ID[] getSectionNames ();            /// Get identifiers for all sections
     void read ();                       /// Commence reading
     void read (ID[] secSet);            /// ditto
-    void read (View!(ID) secSet);       /// ditto
+    void read (IContainer!(ID) secSet);       /// ditto
 }
--- a/mde/font/FontTexture.d	Sun Aug 31 15:59:17 2008 +0100
+++ b/mde/font/FontTexture.d	Thu Sep 11 11:33:51 2008 +0100
@@ -52,11 +52,7 @@
 class FontTexture
 {
     this () {}
-    ~this () {
-        foreach (t; tex) {
-            glDeleteTextures (1, &(t.texID));
-        }
-    }
+    ~this () {}
     
     // Call if font(s) have been changed and glyphs must be recached.
     void clear () {
--- a/mde/font/font.d	Sun Aug 31 15:59:17 2008 +0100
+++ b/mde/font/font.d	Thu Sep 11 11:33:51 2008 +0100
@@ -24,6 +24,7 @@
 import mde.file.mergetag.Reader;
 import mde.file.mergetag.DataSet;
 import mde.setup.paths;
+import mde.setup.exception;     // InitStage stuff
 
 import derelict.freetype.ft;
 import derelict.opengl.gl;
@@ -55,9 +56,9 @@
                 fontTex.drawTexture;
         }
         
-        /** Load the freetype library from the file fileName. */
+        /** Load the freetype library with settings from the file fileName. */
         private const fileName = "fonts";
-        void initialize () {
+        StageState initialize () {
             if (FT_Init_FreeType (&library))
                 throw new fontException ("error initialising the FreeType library");
             
@@ -76,7 +77,7 @@
                 /* An error occurred, presumably because LCD rendering support
                 * is not compiled into the library. */
                 logger.warn ("Bad/unsupported LCD filter option; disabling LCD font rendering.");
-                logger.warn ("Your FreeType 2 library may compiled without support for LCD/sub-pixel rendering.");
+                logger.warn ("Your FreeType 2 library may be compiled without support for LCD/sub-pixel rendering.");
                 
                 // Reset the default filter (in case an invalid value was set in config files).
                 fontOpts.set!(int) ("lcdFilter", FT_LcdFilter.FT_LCD_FILTER_DEFAULT);
@@ -122,12 +123,23 @@
             // Load the fallback now, to ensure it's available.
             // Also note that get() doesn't make sure the fallback is loaded before returning it.
             fallback.load;
+            
+            return StageState.ACTIVE;
         }
         
         //FIXME: don't use GC for FontStyle resources
         /** Cleanup: delete all fonts. */
-        void cleanup () {
-            FT_Done_FreeType (library);
+        StageState cleanup () {
+            // Clear loaded fonts:
+            foreach (fs; fonts)
+                delete fs;
+            fonts = null;
+            delete fallback;
+            
+            delete fontTex;     // clear texture
+            FT_Done_FreeType (library); // free the library
+            
+            return StageState.INACTIVE;
         }
         
         /** Get a FontStyle instance, for a section in the fonts.mtt file.
@@ -285,7 +297,9 @@
     }
     
     ~this () {
+        debug logger.trace ("{}.~this: start", this);
         FT_Done_Face (face);
+        debug logger.trace ("{}.~this: done", this);
     }
     
 private:
--- a/mde/gl/draw.d	Sun Aug 31 15:59:17 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,93 +0,0 @@
-/* LICENSE BLOCK
-Part of mde: a Modular D game-oriented Engine
-Copyright © 2007-2008 Diggory Hardy
-
-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, see <http://www.gnu.org/licenses/>. */
-
-/** The OpenGL draw loop and some basic OpenGL code to set up a projection.
-*
-* Everything here is really intended as makeshift code to enable GUI development. */
-module mde.gl.draw;
-
-import mde.gui.WidgetManager;
-import mde.imde;
-
-import derelict.sdl.sdl;
-import derelict.opengl.gl;
-
-import tango.time.Time;     // TimeSpan (type only; unused)
-import tango.util.log.Log : Log, Logger;
-
-import mde.font.font;
-private Logger logger;
-static this () {
-    logger = Log.getLogger ("mde.gl.draw");
-}
-
-//BEGIN GL & window setup
-void glSetup () {
-    glDisable(GL_LIGHTING);
-    glDisable(GL_DEPTH_TEST);
-    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
-    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
-    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
-    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
-    glEnable(GL_TEXTURE_2D);
-    glShadeModel(GL_SMOOTH);
-    
-    glClearColor (0.0f, 0.0f, 0.0f, 0.0f);
-    
-    glMatrixMode(GL_MODELVIEW);
-    glLoadIdentity();
-    
-    // Used for font rendering:
-    glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
-    glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
-    //NOTE: wrap mode may have an effect, but shouldn't be noticed...
-}
-
-void setProjection (int w, int h) {
-    glMatrixMode (GL_PROJECTION);
-    glLoadIdentity ();
-    
-    glViewport (0,0,w,h);
-    
-    // Make the top-left the origin (see gui/GUI notes.txt):
-    // Note that this only affects vertex operations − direct rasterisation operations are
-    // unaffected!
-    glOrtho (0.0,w, h,0.0, -1.0, 1.0);
-    
-    glMatrixMode(GL_MODELVIEW);
-    
-    // The gui is tied to this viewport.
-    gui.setSize (w,h);
-}
-//END GL & window setup
-
-//BEGIN Drawing loop
-// Temporary draw function
-void draw (TimeSpan) {
-    glClear(GL_COLOR_BUFFER_BIT);
-    
-    gui.draw ();
-    debug (drawGlyphCache) FontStyle.drawTexture;
-    
-    GLenum err = glGetError();
-    while (err != GL_NO_ERROR) {
-        logger.error ("GL error: {}", err);
-        err = glGetError();
-    }
-    
-    glFinish();		// Use Finish rather than Flush to make sure gl is ready to swap buffers
-    SDL_GL_SwapBuffers();
-}
-//END Drawing loop
--- a/mde/gui/WidgetManager.d	Sun Aug 31 15:59:17 2008 +0100
+++ b/mde/gui/WidgetManager.d	Thu Sep 11 11:33:51 2008 +0100
@@ -29,6 +29,7 @@
 import imde = mde.imde;
 import mde.input.Input;
 import mde.scheduler.Scheduler;
+import mde.setup.Screen;
 
 import tango.core.sync.Mutex;
 import tango.util.log.Log : Log, Logger;
@@ -36,13 +37,8 @@
 private Logger logger;
 static this () {
     logger = Log.getLogger ("mde.gui.WidgetManager");
-
-    gui = new WidgetManager ("gui");
 }
 
-WidgetManager gui;
-
-
 /*************************************************************************************************
  * The widget manager.
  * 
@@ -54,7 +50,7 @@
  * 
  * Aside from the IWidgetManager methods, this class should be thread-safe.
  *************************************************************************************************/
-class WidgetManager : WidgetLoader {
+class WidgetManager : WidgetLoader, Screen.Drawable {
     /** Construct a new widget manager.
      * 
      * params:
@@ -62,11 +58,14 @@
      */
     this (char[] file) {
         super(file);
+        
+        Screen.addDrawable (this);
     }
     
     // NOTE - temporarily here to allow CTOR to run safely during static this
     // called during init
     void init () {
+        // Doesn't need a lock - cannot conflict with other class functions.
         // Events we want to know about:
         imde.input.addMouseClickCallback(&clickEvent);
         imde.input.addMouseMotionCallback(&motionEvent);
@@ -75,8 +74,10 @@
     
     /** Draw the gui. */
     void draw() {
+        debug logger.trace ("drawing; w,h = {},{}",w,h);
         synchronized(mutex)
-            child.draw;
+            if (child)
+                child.draw;
     }
     
     
@@ -88,6 +89,7 @@
             logger.warn ("clickEvent: failed!");
         mutex.lock;
         scope(exit) mutex.unlock;
+        if (child is null) return;
         
         // NOTE: buttons receive the up-event even when drag-callbacks are in place.
         foreach (dg; clickCallbacks)
@@ -95,21 +97,10 @@
             if (dg (cast(wdabs)cx, cast(wdabs)cy, b, state)) return;
         
         // NOTE: do we need to test if the click was on the gui (and thus child)?
+        // FIXME: yes, unless we can guarantee this!
         IChildWidget widg = child.getWidget (cast(wdabs)cx,cast(wdabs)cy);
         if (widg !is null)
             widg.clickEvent (cast(wdabs)cx,cast(wdabs)cy,b,state);
-        /+ FIXME: remove
-        foreach (i,w; windows) {
-            IWidget widg = w.getWidget (cast(wdabs)cx,cast(wdabs)cy);
-            if (widg !is null) {
-                // Bring to front
-                windows = w ~ windows[0..i] ~ windows[i+1..$];
-                
-                widg.clickEvent (cast(wdabs)cx,cast(wdabs)cy,b,state);
-                requestRedraw;	// in case we've only moved to front
-                return;     // only pass to first window
-            }
-        }+/
     }
     
     /** For mouse motion events.
@@ -126,15 +117,18 @@
     }
     
     
-    void setSize (int x, int y) {
+    void sizeEvent (int nw, int nh) {   // Drawable function
         mutex.lock;
         scope(exit) mutex.unlock;
         
-        w = cast(wdim) x;
-        h = cast(wdim) y;
+        w = cast(wdim) nw;
+        h = cast(wdim) nh;
         
-        if (child is null)
-            return;     // May not have been created before this is first run.
+        debug logger.trace ("Resize to: {},{}", nw, nh);
+        if (w < mw || h < mh)
+            logger.warn ("Minimal dimensions ({},{}) not met: ({},{}), but I cannot resize myself!",mw,mh,w,h);
+        
+        if (!child) return;     // if not created yet.
         child.setWidth  (w, -1);
         child.setHeight (h, -1);
         child.setPosition (0,0);
@@ -165,7 +159,8 @@
     //END IWidgetManager methods
     
 protected:
-    /* Second stage of widget loading. */
+    /* Second stage of widget loading.
+     * Note: sizeEvent should be called with window size before this. */
     void createRootWidget () {
         // The renderer needs to be created on the first load, but not after this.
         if (rend is null)
@@ -173,6 +168,12 @@
         
         child = makeWidget ("root");
         
+        mw = child.minWidth;
+        mh = child.minHeight;
+        
+        if (w < mw || h < mh)
+            logger.warn ("Minimal dimensions ({},{}) not met: ({},{}), but I cannot resize myself!",mw,mh,w,h);
+        
         child.setWidth  (w, -1);
         child.setHeight (h, -1);
         child.setPosition (0,0);
@@ -184,6 +185,7 @@
     void delegate(wdabs cx, wdabs cy) [void*] motionCallbacks;
     IRenderer rend;
     wdim w,h;       // area available to the widgets
+    wdim mw,mh;     // minimal area available to the widgets
 }
 
 
@@ -212,9 +214,6 @@
         mutex = new Mutex;  // Used on functions intended to be called from outside the gui package.
         fileName = file;
     }
-    ~this () {
-        save;
-    }
     
     /* Load the widgets' data from the file specified to the CTOR.
     * 
@@ -380,7 +379,7 @@
     
     /** Create a widget by ID. */
     IChildWidget makeWidget (widgetID id, IParentWidget parent = null) {
-        debug logger.trace ("Creating widget \""~id~'"');
+        debug (mdeWidgets) logger.trace ("Creating widget \""~id~'"');
         return createWidget (this, curData[id], parent);
     }
     
--- a/mde/gui/widget/Ifaces.d	Sun Aug 31 15:59:17 2008 +0100
+++ b/mde/gui/widget/Ifaces.d	Thu Sep 11 11:33:51 2008 +0100
@@ -124,8 +124,9 @@
  * and is able to communicate with its manager and parent/child widgets as necessary.
  *
  * If a widget is to be creatable by IWidgetManager.makeWidget, it must be listed in the
- * createWidget module, have a constructor of the following form, and should update it's
- * creation data as necessary via IWidgetManager.setData().
+ * createWidget module, and have a constructor of the following form. It should also update it's
+ * creation data if necessary, either when changed or when saveChanges() is called, using
+ * IWidgetManager.setData().
  * It should use Ddoc to explain what initialization data is used.
  * ----------------------------------
  * /++ Constructor for a ... widget.
--- a/mde/gui/widget/createWidget.d	Sun Aug 31 15:59:17 2008 +0100
+++ b/mde/gui/widget/createWidget.d	Thu Sep 11 11:33:51 2008 +0100
@@ -53,7 +53,7 @@
     int type = data.ints[0];    // type is first element of data
     
     //pragma (msg, binarySearch ("type", WIDGETS));
-    mixin (binarySearch ("type", WIDGETS)); // creates widget by type as new *Widget (mgr, data);
+    mixin (binarySearch ("type", WIDGETS)); // creates widget by type: new XWidget (mgr, data [, parent]);
     
     // Not returned a new widget...
     logger.error ("Bad widget type: {}; creating a debug widget instead.",type);
@@ -123,7 +123,7 @@
         char[] ret;
         foreach (c; consts) {
             ret ~=  "if (" ~ var ~ " == WIDGET_TYPE." ~ c ~ ") {\n" ~
-                    "   debug logger.trace (\"Creating new "~c~"Widget.\");\n" ~
+                    "   debug (mdeWidgets) logger.trace (\"Creating new "~c~"Widget.\");\n" ~
                     "   static if (WIDGET_TYPE."~c~" & WIDGET_TYPE.TAKES_PARENT)\n" ~
                     "       return new " ~ c ~ "Widget (mgr, data, parent);\n" ~
                     "   else\n" ~
--- a/mde/gui/widget/layout.d	Sun Aug 31 15:59:17 2008 +0100
+++ b/mde/gui/widget/layout.d	Thu Sep 11 11:33:51 2008 +0100
@@ -428,8 +428,10 @@
                 debug assert (i > 0, "getCell: l < pos[0] (code error)");
                 --i;
             }					// now (l >= pos[i])
-            if (l >= pos[i] + width[i])		// between columns
+            if (l >= pos[i] + width[i]) {	// between columns
+                debug assert (i+1 < minWidth.length, "getCell: l >= pos[$-1] + width[$-1] (code error)");
                 return -i - 1;			// note: i might be 0 so cannot just return -i
+            }
             return i;
         }
         
--- a/mde/input/Config.d	Sun Aug 31 15:59:17 2008 +0100
+++ b/mde/input/Config.d	Thu Sep 11 11:33:51 2008 +0100
@@ -24,7 +24,7 @@
 debug import mde.file.serialize;
 
 import tango.util.log.Log : Log, Logger;
-import tango.util.collection.TreeBag : TreeBag;
+import tango.util.container.HashSet;
 
 /** Class to hold the configuration for the input system. Thus loading and switching between
  *  multiple configurations should be easy.
@@ -115,13 +115,13 @@
     uint[] inheritants;		/// Other profiles to inherit.
     
     static Config[char[]] configs;	/// All configs loaded by load().
-    private static TreeBag!(char[]) loadedFiles;	// all filenames load tried to read
+    private static HashSet!(char[]) loadedFiles;	// all filenames load tried to read
     private static Logger logger;
     
 //BEGIN File loading/saving code
     static this () {
         logger = Log.getLogger ("mde.input.Config");
-        loadedFiles = new TreeBag!(char[]);
+        loadedFiles = new HashSet!(char[]);
     }
     
     // Load all configs from a file.
--- a/mde/input/joystick.d	Sun Aug 31 15:59:17 2008 +0100
+++ b/mde/input/joystick.d	Thu Sep 11 11:33:51 2008 +0100
@@ -18,6 +18,7 @@
 */
 module mde.input.joystick;
 
+import mde.setup.exception;     // InitStage
 import tango.util.log.Log : Log, Logger;
 
 import derelict.sdl.joystick;
@@ -35,25 +36,27 @@
 *
 * closeJoysticks must be run to cleanup afterwards.
 */
-void openJoysticks () {
+StageState openJoysticks () {
     joysticks = new SDL_Joystick*[SDL_NumJoysticks ()];
     
     for (int i = 0; i < joysticks.length; ++i) {
         if ((joysticks[i] = SDL_JoystickOpen (i)) is null) {	// null on failure
             logger.error ("Unable to open joystick {} via SDL", i);
+            return StageState.ERROR;    // prevent closeJoysticks running, but don't halt program
         }
     }
     
     logger.info ("Opened {} joysticks via SDL, succesfully unless preceding errors say otherwise.", joysticks.length);
+    return StageState.ACTIVE;
 }
 
 /// Cleanup fct.
-void closeJoysticks () {
+StageState closeJoysticks () {
     foreach (js; joysticks) {
-        // FIXME: this is sometimes causing a SIGSEGV (Address boundary error)
-        // FIXME: when init fails
+        // FIXME: This sometimes causes a SIGSEGV (Address boundary error) when init fails.
         debug logger.trace ("Closing joysticks (this sometimes fails when mde exits prematurely)");
         if(js !is null) SDL_JoystickClose(js);	// only close if successfully opened
         debug logger.trace ("Done closing joysticks");
     }
+    return StageState.INACTIVE;
 }
--- a/mde/lookup/Options.d	Sun Aug 31 15:59:17 2008 +0100
+++ b/mde/lookup/Options.d	Thu Sep 11 11:33:51 2008 +0100
@@ -136,6 +136,7 @@
             assert (c !is null);    // Instance must be created before calling addOptionsClass
             assert (((cast(ID) i) in subClasses) is null);  // Don't allow a silent replacement
         } body {
+            debug logger.trace ("Adding Options subClass: "~i);
             subClasses[cast(ID) i] = c;
         }
     
@@ -168,15 +169,27 @@
         }
         void save () {
             if (!changed) return;   // no changes to save
-        
+            // Types:
+            // interface IDataSection {...}
+            // class Options {...}
+            // alias char[] ID;
+            // IDataSection[ID] ds.sec;
+            // Options[ID] subClasses;
             DataSet ds = new DataSet();
-            foreach (id, subOpts; subClasses) ds.sec[id] = subOpts.optionChanges;
-        
+            foreach (id, subOpts; subClasses) {
+                ds.sec[id] = subOpts.optionChanges;
+                debug logger.trace ("Saving options section: "~id);
+            }
+            foreach (i,s; ds.sec)
+                debug logger.trace ("sec ID length: {}", i.length);
+            debug logger.trace ("0");
+            
             // Read locally-stored options
             try {
                 IReader reader;
                 reader = confDir.makeMTReader (fileName, PRIORITY.HIGH_ONLY, ds);
                 reader.dataSecCreator = delegate IDataSection(ID id) {
+                    debug logger.trace ("New section to save ignored: "~id);
                     return null;    // All recognised sections are already in the dataset.
                 };
                 reader.read;
@@ -188,9 +201,15 @@
             }
         
             try {
+                debug logger.trace ("1");
                 IWriter writer;
-                writer = confDir.makeMTWriter (fileName, ds);
-                writer.write();
+                foreach (i,s; ds.sec)
+                    debug logger.trace ("sec ID length: {}", i.length);
+                debug logger.trace ("2");
+                writer = confDir.makeMTWriter (fileName, ds);   // FIXME - sometimes SIGSEGV
+                debug logger.trace ("3");
+                writer.write(); // FIXME - this is causing hang at exit!
+                debug logger.trace ("4");
             } catch (Exception e) {
                 logger.error ("Saving options aborted: "~e.msg);
             }
@@ -457,7 +476,7 @@
 /** A home for all miscellaneous options, at least for now. */
 OptionsMisc miscOpts;
 class OptionsMisc : Options {
-    mixin (impl!("bool useThreads, exitImmediately; int logOptions; double pollInterval; char[] L10n, a,b,c;"));
+    mixin (impl!("bool exitImmediately; int numThreads, logOptions; double pollInterval; char[] L10n, a,b,c;"));
     
     static this() {
         miscOpts = new OptionsMisc;
--- a/mde/mde.d	Sun Aug 31 15:59:17 2008 +0100
+++ b/mde/mde.d	Thu Sep 11 11:33:51 2008 +0100
@@ -25,7 +25,7 @@
 import mde.setup.Init;                  // initialization
 import mde.lookup.Options;              // pollInterval option
 import mde.scheduler.Scheduler;         // mainSchedule
-import gl = mde.gl.draw;                // gl.draw()
+import mde.setup.Screen;                // Screen.draw()
 
 import tango.core.Thread : Thread;	// Thread.sleep()
 import tango.time.Clock;                // Clock.now()
@@ -56,7 +56,7 @@
      * not allow running components simultaeneously with threads.
      * Note: probably drawing should start at the beginning of the loop and glFlush()/swapBuffers
      * be called at the end to optimise. */
-    mainSchedule.add (SCHEDULE.DRAW, &gl.draw); // Draw, per event only.
+    mainSchedule.add (SCHEDULE.DRAW, &Screen.draw);     // Draw, per event only.
     mainSchedule.add (mainSchedule.getNewID, &mde.events.pollEvents).frame = true;
     //END Main loop setup
     
--- a/mde/scheduler/Scheduler.d	Sun Aug 31 15:59:17 2008 +0100
+++ b/mde/scheduler/Scheduler.d	Thu Sep 11 11:33:51 2008 +0100
@@ -76,7 +76,7 @@
     *
     * Use the returned pointer to set the scheduling, e.g.:
     * -----
-    * scheduler.add(scheduler.getNewID, myFunction).set(false, TimeSpan.millis (10));
+    * scheduler.add(scheduler.getNewID, myFunction).set(false, TimeSpan.fromMillis (10));
     * scheduler.get(15).frame = true;
     */
     ScheduleFunc add (ID id, scheduleFct func) {
@@ -176,16 +176,16 @@
         void inc1 (TimeSpan) {  ++ctr1; }
         s.add(1,&inc1).frame = true;
         
-        TimeSpan interval = TimeSpan.millis(1);// non-zero so we can check zero after first call
+        TimeSpan interval = TimeSpan.fromMillis(1);// non-zero so we can check zero after first call
         void perInt (TimeSpan i) {  interval = i;    }
-        s.add(2,&perInt).set(false, TimeSpan.millis(10));
+        s.add(2,&perInt).set(false, TimeSpan.fromMillis(10));
         
         Time t = Time.epoch1970;    // starting time (value isn't important)
         s.execute (t);
         assert (ctr1 == 1);         // called once
         assert (interval == TimeSpan.zero); // initial interval
         
-        t += TimeSpan.millis (5);   // 5ms later...
+        t += TimeSpan.fromMillis (5);   // 5ms later...
         s.execute (t);
         assert (ctr1 == 2);
         assert (interval == TimeSpan.zero); // perInt shouldn't get called
@@ -193,22 +193,22 @@
         s.get(1).frame = false;     // don't call per-frame anymore
         s.get(1).request = true;    // but request next call
         
-        t += TimeSpan.millis (5);
+        t += TimeSpan.fromMillis (5);
         s.execute (t);
         assert (ctr1 == 3);         // as requested
-        assert (interval == TimeSpan.millis (10));  // perInt should get called (just!)
+        assert (interval == TimeSpan.fromMillis (10));  // perInt should get called (just!)
         
         s.request(2);               // request this
         
-        t += TimeSpan.millis (8);
+        t += TimeSpan.fromMillis (8);
         s.execute (t);
         assert (ctr1 == 3);         // inc1 shouldn't run
-        assert (interval == TimeSpan.millis (8));  // perInt was requested
+        assert (interval == TimeSpan.fromMillis (8));  // perInt was requested
         
-        t += TimeSpan.millis (4);
+        t += TimeSpan.fromMillis (4);
         s.execute (t);
         // check perInt's last call-time was updated by the request, so it doesn't get run now:
-        assert (interval == TimeSpan.millis (8));
+        assert (interval == TimeSpan.fromMillis (8));
         
         logger.info ("Unittest complete.");
     }
--- a/mde/setup/Init.d	Sun Aug 31 15:59:17 2008 +0100
+++ b/mde/setup/Init.d	Thu Sep 11 11:33:51 2008 +0100
@@ -14,30 +14,50 @@
 along with this program.  If not, see <http://www.gnu.org/licenses/>. */
 
 /**************************************************************************************************
- * Initialisation setup and exit cleanup module.
+ * Initialisation and cleanup (shutdown) module.
  *
- * This module provides an infrastructure for handling much of the initialisation and
- * deinitialisation of the program. It does not, however, provide much of the (de)initialisation
- * code; with the exception of that for the logger.
+ * Program startup follows this sequence: static this() functions, pre-init, init.
+ * Shutdown consists of: cleanup, post-cleanup, static ~this() functions.
+ * 
+ * static this and ~this functions should not use any external resources, such as dynamically
+ * loaded libraries or files, and should not interact with other modules. They should be almost
+ * guaranteed not to fail. Preferably, they shouldn't involve large amounts of memory or
+ * processing, but this is not a requirement.
+ * 
+ * Pre-init: init code written in this module. Generally only prerequisets of most other stages
+ * go here.
+ * 
+ * Init: This is where init code from external modules gets hooked in. Each stage consists of an
+ * initialization function, a corresponding cleanup function, a state, and any dependencies (other
+ * init functions which must be run before this one). Init functions are run according to these
+ * dependencies, potentially threaded simultaeneously with other init functions.
+ * 
+ * Cleanup: Cleanup happens similarly to init for all stages requiring shutdown (according to their
+ * state). The init dependencies are applied in reverse order (so if X depended on Y, Y's cleanup
+ * will not run until X's cleanup has completed), and again the functions may be threaded.
+ * 
+ * Post-cleanup: like pre-init, this is code written in Init.
  *************************************************************************************************/
 module mde.setup.Init;
 
-import mde.setup.init2;     // This module is responsible for setting up some init functions.
-import mde.setup.initFunctions;
+import mde.setup.InitStage;     // Controls external delegates run by init
 import mde.setup.exception;
 
 import mde.lookup.Options;
 import paths = mde.setup.paths;
-import mde.exception;
+import mde.exception;           // optionsLoadException
 
 // tango imports
 import tango.core.Thread;
+import tango.core.sync.Condition;
 import tango.core.Exception;
-import tango.stdc.stringz : fromStringz;
+import tango.util.container.LinkedList;
+
+//import tango.stdc.stringz : fromStringz;
 import tango.io.Console;	// for printing command-line usage
 import TimeStamp = tango.text.convert.TimeStamp, tango.time.WallClock;	// output date in log file
 import tango.util.Arguments;
-import tango.util.log.Log : Log, Logger, Level;
+import tango.util.log.Log;
 import tango.util.log.AppendConsole;
 import tango.util.log.AppendFiles;
 
@@ -48,60 +68,32 @@
 import derelict.freetype.ft;
 import derelict.util.exception;
 
-/**
- * Static CTOR
- *
- * This should handle a minimal amount of functionality where useful. For instance, configuring the
- * logger here and not in Init allows unittests to use the logger.
- */
-static this()
-{
-    Logger root = Log.root;
-    // Set the level here, but set it again once options have been loaded:
-    debug root.level(Logger.Trace);
-    else  root.level(Logger.Info);
-    // Temporarily log to the console (until we've found paths and loaded options):
-    root.add(new AppendConsole);
-}
-static ~this()
-{
-}
 
-/**
+/**************************************************************************************************
  * Init class
  *
  * A scope class created at beginning of the program and destroyed at the end; thus the CTOR
  * handles program initialisation and the DTOR handles program cleanup.
- */
+ *************************************************************************************************/
 scope class Init
 {
-    //private bool failure = false;       // set true on failure during init, so that clean
-    private static Logger logger;
     static this() {
+        // Set up the logger temporarily (until pre-init):
+        Logger root = Log.root;
+        debug root.level(Logger.Trace);
+        else  root.level(Logger.Info);
+        root.add(new AppendConsole);
+        
         logger = Log.getLogger ("mde.setup.Init");
     }
     
-    /** this() − initialisation
-    *
-    * Runs general initialisation code, in a threaded manner where this isn't difficult.
-    * If any init fails, cleanup is still handled by ~this().
-    *
-    * Init order: 1. Pre-init (loads components needed by most init functions). 2. Dynamic library
-    * loading (load any dynamic libraries first, so that if loading fails nothing else need be
-    * done). 3. Init functions (threaded functions handling the rest of initialisation).
-    */
-    /* In a single-threaded function this could be done with:
-    * scope(failure) cleanup;
-    * This won't work with a threaded init function since any threads completing succesfully will
-    * not clean-up, and a fixed list of clean-up functions cannot be used since clean-up functions
-    * must not run where the initialisation functions have failed.
-    * Hence a list of clean-up functions is built similarly to scope(failure) --- see addCleanupFct.
-    */
+    /** this() − pre-init and init */
     this(char[][] cmdArgs)
     {
-        debug logger.trace ("Init: starting");
-        
-        //BEGIN Pre-init (stage init0)
+        /******************************************************************************************
+         * Pre-init - init code written in this module.
+         *****************************************************************************************/
+        debug logger.trace ("Init: starting pre-init");
         //FIXME: warn on invalid arguments, including base-path on non-Windows
         // But Arguments doesn't support this (in tango 0.99.6 and in r3563).
         Arguments args;
@@ -210,23 +202,21 @@
         }
         debug logger.trace ("Init: dynamic libraries loaded");
         //END Load dynamic libraries
-        //END Pre-init
         
         
-        //BEGIN Init (stages init2, init4)
-        /* Call init functions.
-        *
-        * Current method is to try using threads, and on failure assume no threads were actually
-        * created and run functions in a non-threaded manner. */
+        /******************************************************************************************
+         * Init − where init code from external modules gets hooked in.
+         *****************************************************************************************/
+        debug logger.trace ("Init: done pre-init, starting init stages");
         
-        try {
-            if (runStageThreaded (init)) runStageForward (init);
-        }
-        catch (InitStageException) {    // This init stage failed.
-            // FIXME: check DTOR still runs
-            throw new InitException ("An init function failed (see above message(s))");
-        }
-        //END Init
+        // Calculate reverse dependencies of stages:
+        foreach (key,stage_p; stages)
+            foreach (name; stage_p.depends)
+                stages[name].rdepends ~= key;
+        if (miscOpts.numThreads < 1 || miscOpts.numThreads > 64)        // limit to a sensible number of threads
+            miscOpts.set!(int)("numThreads", 4);        // FIXME enforce limit in Options
+        
+        runStages!(true);       // startup delegates
         
         debug logger.trace ("Init: done");
     }
@@ -236,113 +226,153 @@
     {
         debug logger.trace ("Cleanup: starting");
         
-        Options.save(); // save options... do so here for now
+        runStages!(false);      // cleanup delegates
         
-        // General cleanup:
-        try {
-            if (runStageThreaded (cleanup)) runStageReverse (cleanup);
-        }
-        catch (InitStageException) {
-            // Nothing else to do but report:
-            logger.error ("One or more cleanup functions failed!");
-        }
+        Options.save(); // save options... do so here for now
         
         debug logger.trace ("Cleanup: done");
     }
     
-    
-    //BEGIN runStage...
-    private static {
-        /* The following three functions, runStage*, each run all functions in a stage in some order,
-        * catching any exceptions thrown by the functions (although this isn't guaranteed for threads),
-        * and throw an InitStageException on initFailure. */
-    
-        const LOG_IF_MSG = "Init function ";
-        const LOG_CF_MSG = "Cleanup function ";
-        const LOG_F_START = " - running";
-        const LOG_F_END = " - completed";
-        const LOG_F_BAD = " - failed";
-        const LOG_F_FAIL = " - exception: ";
-        /* Runs all functions consecutively, first-to-last.
-        * If any function fails, halts immediately. */
-        void runStageForward (InitStage s) {
-            foreach (func; s.funcs) {
-                if (initFailure) break;
-                try {
-                    debug logger.trace (LOG_IF_MSG ~ func.name ~ LOG_F_START);
-                    func.func();
-                    debug logger.trace (LOG_IF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END));
-                } catch (Exception e) {
-                    logger.fatal (LOG_IF_MSG ~ func.name ~ LOG_F_FAIL ~
-                            ((e.msg is null || e.msg == "") ? "(no failure message)" : e.msg) );
-                
-                    setInitFailure();
+    // run init stages or cleanup if startup is false
+    private static void runStages(bool startup) () {
+        LinkedList!(InitStage*) toRun = new LinkedList!(InitStage*);
+        foreach (v; stages) {
+            // Filter only stages with the relevant delegate. It is not checked later that the
+            // delegate exists!
+            // If no init/cleanup function exists, implicit activation/deactivation can occur so
+            // that dependents/dependencies may activate/deactivate.
+            static if (startup) {
+                if (v.state == StageState.INACTIVE) {
+                    if ((*v).init)
+                        toRun.append (v);
+                    else
+                        v.state = StageState.ACTIVE;
                 }
-            }
-            
-            if (initFailure) throw new InitStageException;    // Problem running; abort and cleanup from here.
-        }
-        /* Runs all functions consecutively, last-to-first.
-        * If any function fails, continue until all have been run. */
-        void runStageReverse (InitStage s) {
-            foreach_reverse (func; s.funcs) {
-                try {
-                    debug logger.trace (LOG_CF_MSG ~ func.name ~ LOG_F_START);
-                    func.func();
-                    debug logger.trace (LOG_CF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END));
-                } catch (Exception e) {
-                    logger.fatal (LOG_CF_MSG ~ func.name ~ LOG_F_FAIL ~
-                            ((e.msg is null || e.msg == "") ? "(no failure message)" : e.msg) );
-                
-                    setInitFailure();
+            } else {
+                if (v.state == StageState.ACTIVE) {
+                    if (v.cleanup)
+                        toRun.append (v);
+                    else
+                        v.state = StageState.INACTIVE;
                 }
             }
-            if (initFailure) throw new InitStageException;    // Problem running; abort and cleanup from here.
         }
-        /* Tries running functions in a threaded way. Returns false if successful, true if not but
-        * functions should be run without threads. */
-        bool runStageThreaded (InitStage s) {
-            if (!miscOpts.useThreads) return true;  // Use unthreaded route instead
+        debug logger.trace ("Added {} init/cleanup stages", toRun.size);
+        int numWorking = miscOpts.numThreads;
+        bool abortInit = false;
+        Mutex toRunM = new Mutex;       // synchronization on toRun, numWorking
+        Condition toRunC = new Condition(toRunM);       // used by threads waiting for remaining stages' dependencies to be met
         
-            ThreadGroup tg;
-            try {                           // creating/starting threads could fail
-                tg = new ThreadGroup;
-                foreach (func; s.funcs) {   // Start all threads
-                    debug logger.trace (LOG_IF_MSG ~ func.name ~ LOG_F_START);
-                    tg.create(func.func);
-                    debug logger.trace (LOG_IF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END));
-                }
-            } catch (ThreadException e) {   // Problem with threading; try without threads
-                logger.error ("Caught ThreadException while trying to create threads:");
-                logger.error (e.msg);
-                logger.info ("Will disable threads and continue, assuming no threads were created.");
+        /* This is a threadable member function to run init stages.
+         * Operation follows:
+         * 1 Look for a stage to run:
+         *   if found:
+         *      notify waiting threads work may be available
+         *      init stage
+         *      goto 1
+         *   if not found:
+         *      if any other threads are working, wait (dependencies may not be met yet)
+         *      if no other threads are working, notify waiting threads and terminate (finished)
+         * When notified, threads start at 1. */
+        void initThreadFct () {
+            debug logger.trace ("initThreadFct: starting");
+            try {       // created as a thread - must not throw exceptions
+            InitStage* stage;
             
-                miscOpts.set!(bool)("useThreads", false);   // Disable threads entirely
-                return true;                // Try again without threads
-            }
-        
-            /* Wait for all threads to complete.
-            *
-            * If something went wrong, we still need to do this before cleaning up.
-            */
-            foreach (t; tg) {
-                try {
-                    t.join (true);
-                } catch (Exception e) {
-                    // Relying on catching exceptions thrown by other threads is a bad idea.
-                    // Hence all threads should catch their own exceptions and return safely.
+            threadLoop: while (true) {      // thread loops until a problem occurs or nothing else can be done
+            // Look for a job:
+            synchronized (toRunM) {
+                --numWorking;           // stopped working: looking/waiting for a job
+                if (abortInit) break threadLoop;  // something went wrong in another thread
+                static if (startup)
+                    int num_rdepends = (stage is null) ? 0 : stage.rdepends.length;
+                else
+                    int num_rdepends = (stage is null) ? 0 : stage.depends.length;
                 
-                    logger.fatal ("Unhandled exception from Init function:");
-                    logger.fatal (e.msg);
-                
-                    setInitFailure ();        // abort (but join other threads first)
+                getStage: while (true) {
+                    auto it = toRun.iterator;   // asserts if toRun is empty
+                    itStages: while (it.next (stage)) {   // get next element of toRun
+                        static if (startup) {
+                            foreach (d; stage.depends)
+                                if (stages[d].state != StageState.ACTIVE)
+                                    continue itStages;  // dependency isn't met (yet)
+                        } else {
+                            foreach (d; stage.rdepends)
+                                if (stages[d].state != StageState.INACTIVE)
+                                    continue itStages;  // reverse dependency isn't unmet (yet)
+                        }
+                        
+                        // All dependencies met
+                        it.remove;
+                        break getStage;
+                    }
+                    
+                    // No stage remaining with all dependencies met
+                    if (!toRun.isEmpty && numWorking)     // still some working so more dependencies may be met later
+                        toRunC.wait;    // wait until another thread finishes
+                    else                // no thread is working, so none of what's left is doable, or nothing's left
+                        break threadLoop;
                 }
+                ++numWorking;           // got a job!
+                if (num_rdepends > 2)   // how many stages depended on the last one run?
+                    toRunC.notifyAll(); // tell all waiting threads there may be work now
+                else if (num_rdepends == 2)
+                    toRunC.notify();    // there's potentially work for this thread and one other
+                // else there won't be additional work so don't notify
             }
             
-            if (initFailure) throw new InitStageException;    // Problem running; abort and cleanup from here.
-            return false;                   // Done successfully
+            // Do a job:
+            try {
+                // FIXME - old stage start&finish trace messages - we don't have a name!
+                debug logger.trace ("InitStage {}: starting", stage);
+                static if (startup)
+                    stage.state = (*stage).init();  // init is a property of a pointer (oh no!)
+                else
+                    stage.state = stage.cleanup();
+                debug logger.trace ("InitStage {}: completed; state: {}", stage, stage.state);
+            } catch (InitStageException e) {
+                debug logger.trace ("InitStage {}: failed", stage);
+                stage.state = e.state;
+                abortInit = true;
+                break threadLoop;
+            } catch (Exception e) {
+                debug logger.trace ("InitStage {}: failed", stage);
+                abortInit = true;
+                break threadLoop;
+            }
+            }
+            } catch (Exception e) {
+                logger.fatal ("Exception in initThreadFct: "~e.msg);
+                abortInit = true;
+            }
+            toRunC.notifyAll(); // Most likely if we're exiting, we should make sure others aren't waiting.
+            debug logger.trace ("initThreadFct: returning");
+            return;
         }
-    //END runStage...
+        
+        // Start miscOpts.NumThreads - 1 threads:
+        try {
+            ThreadGroup g = new ThreadGroup;
+            for (int i = miscOpts.numThreads; i > 1; --i)
+                g.create (&initThreadFct);
+            initThreadFct();    // also run in current thread
+            g.joinAll (false);  // don't rethrow exceptions - there SHOULD NOT be any
+        } catch (ThreadException e) {
+            logger.error ("Exception while using threads: "~e.msg);
+            logger.error ("Disabling threads and attempting to continue.");
+            miscOpts.set!(int)("NumThreads", 1);        // count includes current thread
+            initThreadFct();                            // try with just this thread
+        }       // any other exception will be caught in main() and abort program
+        
+        if (abortInit)
+            throw new InitException ("An init/cleanup function failed (see above message(s))");
+        
+        foreach (stage; toRun)
+            logger.warn ("InitStage {}: was not run due to unmet dependencies", stage);
+    }
+    
+    private static {
+        Logger logger;
         
         void printUsage (char[] progName) {
             Cout ("mde [no version]").newline;
@@ -366,32 +396,90 @@
     }
     
     debug (mdeUnitTest) unittest {
-        /* Fake init and cleanup. Use unittest-specific init and cleanup InitStages to avoid
-        * messing other init/cleanup up. */
-        static InitStage initUT, cleanupUT;
+        logger.trace ("Starting unittest");
+        
+        auto realInit = stages;         // backup the real init stages
+        stages = new typeof(stages);    // an empty test-bed
+        
+        bool init1, init2, init3 = true;
+        bool failed = false;    // set if anything goes wrong
+        StageState s1InitReturns = StageState.ACTIVE;
+        addInitStage ("stg1", delegate StageState() {
+            init1 = true;
+            return s1InitReturns;
+        }, delegate StageState() {
+            init1 = false;
+            return StageState.INACTIVE;
+        });
+        addInitStage ("stg2", delegate StageState() {
+            if (!init1) failed = true;
+            init2 = true;
+            return StageState.ACTIVE;
+        }, delegate StageState() {
+            if (!init1) failed = true;
+            init2 = false;
+            return StageState.INACTIVE;
+        }, ["stg1"]);
+        InitStage s3;
+        s3.init = delegate StageState() {
+            throw new InitStageException (cast(StageState)7);   // not a normal state, but good for a test
+            return StageState.ERROR;
+        };
+        s3.cleanup = delegate StageState() {
+            if (!init1) failed = true;
+            init3 = false;
+            return StageState.INACTIVE;
+        };
+        s3.depends = [ toStageName("stg1") ];
+        s3.state = StageState.ACTIVE;   // already active, so s3.init should not run (first time)
+        addInitStage ("stg3", &s3);
         
-        static bool initialised = false;
-        static void cleanupFunc1 () {
-            initialised = false;
-        }
-        static void cleanupFunc2 () {
-            assert (initialised == true);
-        }
-                
-        static void initFunc () {
-            initialised = true;
-            cleanupUT.addFunc (&cleanupFunc1, "UT cleanup 1");
-            cleanupUT.addFunc (&cleanupFunc2, "UT cleanup 2");
-        }
+        // Stuff normally done in Init.this():
+        // Calculate reverse dependencies of stages:
+        foreach (key,stage_p; stages)
+            foreach (name; stage_p.depends)
+                stages[name].rdepends ~= key;
+        if (miscOpts.numThreads < 1 || miscOpts.numThreads > 64)        // limit to a sensible number of threads
+            miscOpts.set!(int)("numThreads", 4);        // FIXME enforce limit in Options
+        
+        
+        // Run the above.
+        runStages!(true);
+        assert (init1);
+        assert (init2);
+        assert (!failed);
+        foreach (s; stages)
+            assert (s.state == StageState.ACTIVE);
         
-        initUT.addFunc (&initFunc, "UT init");
+        runStages!(false);
+        assert (!init1);
+        assert (!init2);
+        assert (!init3);
+        assert (!failed);
+        foreach (s; stages)
+            assert (s.state == StageState.INACTIVE);
         
-        runStageForward (initUT);
-        assert (initialised);
+        s1InitReturns = StageState.ERROR;
+        // Run again. S2/S3 shouldn't run, S1 won't shut down
+        runStages!(true);
+        assert (init1);
+        assert (!init2);
+        assert (!init3);
+        assert (!failed);
+        runStages!(false);
+        assert (init1); // S1 cleanup won't run
         
-        runStageReverse (cleanupUT);
-        assert (!initialised);
-
+        stages[toStageName("stg1")].state = StageState.INACTIVE;     // hack it back so we can still test
+        s1InitReturns = StageState.ACTIVE;
+        init1 = false;
+        try {
+            runStages!(true);
+            assert (false);             // runStages should throw because s3.init runs now
+        } catch (Exception) {}
+        assert (init1); // s1.init should run first
+        assert (stages[toStageName("stg3")].state == cast(StageState)7);        // set by the exception
+        
+        stages = realInit;      // restore the real init stages
         logger.info ("Unittest complete.");
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/setup/InitStage.d	Thu Sep 11 11:33:51 2008 +0100
@@ -0,0 +1,110 @@
+/* LICENSE BLOCK
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+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, see <http://www.gnu.org/licenses/>. */
+
+/**************************************************************************************************
+ * The infrastructure for handling external startup/shutdown code.
+ *************************************************************************************************/
+module mde.setup.InitStage;
+
+public import mde.setup.exception;
+import tango.util.container.HashMap;
+import tango.util.log.Log : Log, Logger;
+
+    // use as hash type for better performance than char[]
+    alias uint StageName;
+    StageName toStageName (char[4] x) {
+        return *cast(uint*) x.ptr;      // NOTE - little hack to read as a uint
+    }
+    
+    /** Initialization and cleanup functions for one stage.
+     * 
+     * The init and cleanup functions should return the new StageState. Returning ERROR does not
+     * indicate that the program should abort but that the module in question is not usable and
+     * that init & cleanup should not be called. Throwing an exception sets the state to ERROR (or
+     * the value passed to an InitStageException) and indicates that the program should abort.
+     * If the program aborts, all stages with state ACTIVE have their cleanup run (as with a normal
+     * shutdown).
+     * 
+     * Not setting a cleanup function will result in the state being left at ACTIVE on shutdown, so
+     * if a second initialization occurs (not currently possible), init will not be re-run. */
+    struct InitStage {
+        StageState delegate() init;     // the initialization function
+        StageState delegate() cleanup;  // the associated cleanup function
+        StageName[] depends;            // anything function depends on completing before its run
+        StageName[] rdepends;           // reverse dependencies, set during init
+        StageState state = StageState.INACTIVE;
+    }
+    
+    /// Add a stage to be initialized.
+    void addInitStage (char[4] name, InitStage* stage) {
+        stages[toStageName(name)] = stage;
+    }
+    /// Add a stage to be initialized.
+    void addInitStage (char[4] name, StageState function() init, StageState function() cleanup = null, char[4][] depends = null) {
+        StageState delegate() i,c;
+        i.funcptr = init;
+        c.funcptr = cleanup;
+        addInitStage (name, i, c, depends);
+    }
+    /// Add a stage to be initialized.
+    void addInitStage (char[4] name, StageState delegate() init, StageState delegate() cleanup = null, char[4][] depends = null) {
+        InitStage* stage = new InitStage;
+        (*stage).init = init;
+        stage.cleanup = cleanup;
+        stage.depends.length = depends.length;
+        foreach (i,d; depends)
+            stage.depends[i] = toStageName(d);
+        stages[toStageName(name)] = stage;
+    }
+    
+    package HashMap!(StageName,InitStage*) stages;
+    
+    static this () {
+        stages = new typeof(stages);
+        logger = Log.getLogger ("mde.setup.InitStage");
+    }
+    Logger logger;
+
+
+
+/**************************************************************************************************
+ * Initialization functions.
+ *************************************************************************************************/
+import imde = mde.imde;
+import mde.input.Input;
+import mde.setup.Screen;
+import mde.input.joystick;
+import mde.font.font;
+
+static this() {
+    addInitStage ("Inpt", &initInput);
+    addInitStage ("SSDL", &Screen.init, &Screen.cleanup );
+    addInitStage ("SJoy", &openJoysticks, &closeJoysticks, ["SSDL"]);
+    addInitStage ("SWnd", &Screen.initWindow, null, ["SSDL"]);
+    addInitStage ("Font", &FontStyle.initialize, &FontStyle.cleanup);
+}
+
+StageState initInput () {
+    imde.input.loadConfig ("input");
+    
+    // Quit on escape. NOTE: quit via SDL_QUIT event is handled completely independently!
+    imde.input.addButtonCallback (cast(Input.inputID) 0x0u, delegate void(Input.inputID i, bool b) {
+        if (b) {
+            logger.info ("Quiting...");
+            imde.run = false;
+        }
+    } );
+    return StageState.ACTIVE;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/setup/Screen.d	Thu Sep 11 11:33:51 2008 +0100
@@ -0,0 +1,269 @@
+/* LICENSE BLOCK
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+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, see <http://www.gnu.org/licenses/>. */
+
+/** Screen: SDL & OpenGL setup/cleanup, drawing.
+ *
+ * Has an interface by which other code can hook in for drawing and resize notifications. */
+module mde.setup.Screen;
+
+import mde.setup.exception;
+import mde.lookup.Options;
+import imde = mde.imde;
+debug (drawGlyphCache) import mde.font.font;
+
+import tango.util.log.Log : Log, Logger;
+import tango.stdc.stringz;
+import tango.time.Time;     // TimeSpan (type only; unused)
+
+import derelict.sdl.sdl;
+import derelict.opengl.gl;	// for loading a later gl version
+import derelict.util.exception;
+
+/** Currently just used as a namespace. Potential for multiple screen support later? */
+struct Screen {
+    // TYPES (these mustn't be static):
+    /** Interface for anything hooking into the screen for drawing, etc. */
+    interface Drawable {
+        /** Called on window creation and whenever the window manager resizes the window, before
+         * the new size is set. The new size is passed.
+         *
+         * The parameters were passed as ref to allow the called function to change them, but SDL
+         * didn't appear able to resize the window (on X11), so this was dropped. */
+        void sizeEvent (int w, int h);
+        
+        /** Called you guess when :-) */
+        void draw ();
+    }
+    
+    /** All video options. */
+    class OptionsVideo : Options {
+        mixin (impl!("bool fullscreen,hardware,resizable,noFrame; int screenW,screenH,windowW,windowH;"));
+    }
+    
+static:
+    /** Init function to initialize SDL. */
+    StageState init () {      // init func
+        // Initialise SDL
+        if (SDL_Init (SDL_INIT_VIDEO | SDL_INIT_JOYSTICK /+| SDL_INIT_EVENTTHREAD+/)) {
+            logger.fatal ("SDL initialisation failed:");
+            char* msg = SDL_GetError ();
+            logger.fatal (msg ? fromStringz(msg) : "no reason available");
+            
+            throw new InitException ("SDL Initialization failed");
+        }
+        debug logger.trace ("SDL initialised");
+        return StageState.ACTIVE;
+    }
+    /** SDL shutdown */
+    StageState cleanup () {
+        SDL_Quit();
+        return StageState.INACTIVE;
+    }
+    /** Init function to set up a window with OpenGL support. */
+    StageState initWindow () {
+        //BEGIN Create window and initialize OpenGL
+        // Window creation flags and size
+        flags = SDL_OPENGL;
+        if (vidOpts.hardware) flags |= SDL_HWSURFACE | SDL_DOUBLEBUF;
+        else flags |= SDL_SWSURFACE;
+        int w, h;
+        if (vidOpts.fullscreen) {
+            flags |= SDL_FULLSCREEN;
+            w = vidOpts.screenW;
+            h = vidOpts.screenH;
+        }
+        else {
+            if (vidOpts.resizable) flags |= SDL_RESIZABLE;
+            if (vidOpts.noFrame) flags |= SDL_NOFRAME;
+            w = vidOpts.windowW;
+            h = vidOpts.windowH;
+        }
+        
+        // OpenGL attributes
+        SDL_GL_SetAttribute(SDL_GL_RED_SIZE,    5);
+        SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE,  6);
+        SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE,   5);
+        SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE,  16);
+        SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER,1);
+        
+        // Open a window
+        debug logger.trace ("Opening a window (this can crash if the libraries are messed up)");
+        if (setWindow (w, h)) {
+            throw new InitException ("Failed to open a window");
+        }
+        
+        /* Now (must be done after GL context is created) we can try to load later version.
+         * The initial loading provides opengl 1.1 features.
+         *
+         * 1.4 is now used for glBlendColor (coloured text).
+         *
+         * Currently the latest version used is 1.3; adjust this as necessary. However, before using
+         * features from any OpenGL version > 1.1 a check must be made on what was loaded by calling
+         * DerelictGL.availableVersion(). Note that availableVersion() could be used instead to load
+         * the highest supported version but this way we know what we're getting. */
+        if (DerelictGL.availableVersion < GLVersion.Version13) {
+            throw new InitException ("Required at least OpenGL 1.3; didn't get this.");
+        }
+        /+try {
+            DerelictGL.loadVersions(GLVersion.Version14);
+        } catch (SharedLibProcLoadException e) {
+            logger.warn ("Loading OpenGL version 1.4 failed:");
+            logger.warn (e.msg);
+            
+            setInitFailure ();
+            return;
+        }+/
+        
+        // OpenGL stuff:
+        glDisable(GL_LIGHTING);
+        glDisable(GL_DEPTH_TEST);
+        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
+        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
+        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+        glEnable(GL_TEXTURE_2D);
+        glShadeModel(GL_SMOOTH);
+        
+        glClearColor (0.0f, 0.0f, 0.0f, 0.0f);
+        
+        glMatrixMode(GL_MODELVIEW);
+        glLoadIdentity();
+        
+        // Used for font rendering:
+        glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+        glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+        //NOTE: wrap mode may have an effect, but shouldn't be noticed...
+        
+        // Window-manager settings
+        SDL_WM_SetCaption (toStringz ("mde"), null);
+        // SDL_WM_GrabInput (use later)
+        //END Create window and initialize OpenGL
+        return StageState.ACTIVE;
+    }
+    
+    
+    /** Called when a resize event occurs (when the window manager resizes the window). */
+    void resizeEvent (int w, int h) {
+        // Save new size to config
+        if (vidOpts.fullscreen) {       // probably resizeEvent only called when not fullscreen
+            vidOpts.set!(int) ("screenW", w);
+            vidOpts.set!(int) ("screenH", h);
+        } else {
+            vidOpts.set!(int) ("windowW", w);
+            vidOpts.set!(int) ("windowH", h);
+        }
+        
+        if (setWindow (w,h))
+            imde.run = false;
+    }
+    
+    /** Add a drawable element to the screen (see Drawable interface).
+     *
+     * Should be called before Init to get initial size. Currently no means to remove drawables,
+     * and not really designed for more than one. */
+    void addDrawable (Drawable d) {
+        drawables ~= d;
+    }
+    
+    /** Drawing function */
+    void draw (TimeSpan) {
+        glClear(GL_COLOR_BUFFER_BIT);
+        
+        foreach (Drawable d; drawables)
+            d.draw;
+        
+        debug (drawGlyphCache) {
+            logger.trace ("Drawing font texture");
+            FontStyle.drawTexture;
+        }
+        
+        // Error check:
+        GLenum err = glGetError();
+        while (err != GL_NO_ERROR) {
+            logger.error ("GL error: {}", err);
+            err = glGetError();
+        }
+        
+        glFinish();         // Use Finish rather than Flush to make sure gl is ready to swap buffers
+        SDL_GL_SwapBuffers();
+    }
+    
+    /** Set a new window size. Returns true on failure due to the different ways this must be
+     * handled. */
+    private bool setWindow (int w, int h) {
+        foreach (d; drawables)  // Tell all drawables the new window size.
+            d.sizeEvent (w,h);
+        
+        debug logger.trace ("Setting video mode {}x{}, 32-bit, flags: {}", w,h,flags);
+        if (SDL_SetVideoMode (w, h, 32, flags) is null) {
+            logger.fatal ("Unable to set video mode:");
+            char* msg = SDL_GetError ();
+            logger.fatal (msg ? fromStringz(msg) : "no reason available");
+            
+            // Print a load of info:
+            logger.info ("Available video modes:");
+            SDL_Rect** modes = SDL_ListModes (null, SDL_FULLSCREEN);
+            if (modes is null) logger.info ("None!");
+            else if (modes is cast(SDL_Rect**) -1) logger.info ("All modes are available");
+            else {
+                for (uint i = 0; modes[i] !is null; ++i) {
+                    logger.info ("\t{}x{}", modes[i].w, modes[i].h);
+                }
+            }
+            
+            SDL_VideoInfo* vi = SDL_GetVideoInfo ();
+            if (vi !is null) {
+                logger.info ("Video info:");
+                logger.info ("Hardware surface support: "~ (vi.flags & SDL_HWSURFACE ? "yes" : "no"));
+                logger.info ("Video memory: {}", vi.video_mem);
+                
+                if (vi.vfmt !is null) {
+                    logger.info ("Best video mode:");
+                    logger.info ("Bits per pixel: {}", vi.vfmt.BitsPerPixel);
+                }
+            }
+            
+            return true;
+        }
+        
+        // Reset the projection and viewport
+        glMatrixMode (GL_PROJECTION);
+        glLoadIdentity ();
+        
+        glViewport (0,0,w,h);
+        
+        // Make the top-left the origin (see gui/GUI notes.txt):
+        // Note that this only affects vertex operations − direct rasterisation operations are
+        // unaffected!
+        glOrtho (0.0,w, h,0.0, -1.0, 1.0);
+        
+        glMatrixMode(GL_MODELVIEW);
+        return false;
+    }
+    
+    static this() {
+        logger = Log.getLogger ("mde.setup.Screen");
+        
+        vidOpts = new OptionsVideo;
+        Options.addOptionsClass (vidOpts, "video");
+    }
+    
+    // DATA:
+private:
+    uint flags = 0;
+    Drawable[] drawables;
+    Logger logger;
+    OptionsVideo vidOpts;
+}
--- a/mde/setup/exception.d	Sun Aug 31 15:59:17 2008 +0100
+++ b/mde/setup/exception.d	Thu Sep 11 11:33:51 2008 +0100
@@ -13,7 +13,8 @@
 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>. */
 
-/// Contains the exception classes for Init.
+/** Contains the exception classes for Init, and the StageState type to remove the need for init
+ * functions to import other Init modules. */
 module mde.setup.exception;
 
 import mde.exception;
@@ -29,9 +30,18 @@
     }
 }
 
-/// Thrown when an init stage fails.
+/// State of an InitStage.
+enum StageState : byte {
+    INACTIVE = 0,                   /// run init on startup
+    ACTIVE = 1,                     /// run cleanup on shutdown
+    ERROR = -1,                     /// locked; don't run init or cleanup
+}
+
+/// May be thrown when an init or cleanup function fails to explicitly set the stage's state.
 class InitStageException : InitException {
-    this () {
+    this (StageState s) {
+        state = s;
         super("");
     }
+    StageState state;
 }
--- a/mde/setup/init2.d	Sun Aug 31 15:59:17 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,101 +0,0 @@
-/* LICENSE BLOCK
-Part of mde: a Modular D game-oriented Engine
-Copyright © 2007-2008 Diggory Hardy
-
-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, see <http://www.gnu.org/licenses/>. */
-
-/** This module is the start of implementing the following:
-*
-* Idea: change import direction so this module adds all init functions. All init functions are
-* wrapped in another function before being run in a thread (i.e. run indirectly). Functions fail
-* either by throwing an exception or by returning a boolean. Functions may take parameters, e.g.
-* "out cleanupFunc[]".
-*
-* This should make it much easier to tell what actually happens during init and to order init such
-* that dependencies are honoured.
-*
-* Currently some external modules depend on InitFunctions, while some are set up from here. Once
-* all are set up from here, the Init* modules can be rearranged. */
-/* Idea: go back to init being controlled from elsewhere. Add a function to wait for another init
- * function to complete (threaded; might need to be done differently for non-threaded). */
-module mde.setup.init2;
-
-import mde.setup.initFunctions;
-
-import tango.util.log.Log : Log, Logger;
-
-// Modules requiring init code running:
-import imde = mde.imde;
-import mde.gui.WidgetManager;
-import mde.input.Input;
-import font = mde.font.font;
-
-// NOTE: error reporting needs a revision
-
-private Logger logger;
-static this () {
-    logger = Log.getLogger ("mde.setup.init2");
-    
-    init.addFunc (&initInput, "initInput");
-    init.addFunc (&guiLoad, "guiLoad");
-}
-
-void guiLoad () {   // init func
-    try {
-        font.FontStyle.initialize;
-        gui.init;
-        gui.loadDesign();
-        cleanup.addFunc (&guiSave, "guiSave");
-    } catch (Exception e) {
-        logger.fatal ("guiLoad failed: " ~ e.msg);
-        setInitFailure;
-    }
-}
-void guiSave () {   // cleanup func
-    try {
-        gui.save;   // NOTE: can save() just be called from DTOR?
-    } catch (Exception e) {
-        logger.fatal ("guiSave failed: " ~ e.msg);
-        setInitFailure;
-    }
-}
-
-void initInput () { // init func
-    try {
-        imde.input.loadConfig ("input");
-        
-        // Quit on escape. NOTE: quit via SDL_QUIT event is handled completely independently!
-        imde.input.addButtonCallback (cast(Input.inputID) 0x0u, delegate void(Input.inputID i, bool b) {
-            if (b) {
-                logger.info ("Quiting...");
-                imde.run = false;
-            }
-        } );
-    } catch (Exception e) {
-        logger.fatal ("initInput failed: " ~ e.msg);
-        setInitFailure;
-    }
-}
-
-/+ Potential wrapper function:
-// Template to call function, catching exceptions:
-void wrap(alias Func) () {
-    try {
-        Func();
-    } catch (Exception e) {
-        logger.fatal (FAIL_MSG);
-        logger.fatal (e.msg);
-        setInitFailure;
-    }
-}
-private const FAIL_MSG = "Unexpected exception caught:";
-+/
--- a/mde/setup/initFunctions.d	Sun Aug 31 15:59:17 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-/* LICENSE BLOCK
-Part of mde: a Modular D game-oriented Engine
-Copyright © 2007-2008 Diggory Hardy
-
-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, see <http://www.gnu.org/licenses/>. */
-
-/** This module is responsible for calling all init functions.
-*
-* It is also responsible for setting up all scheduled functions for now.
-
-* Idea: change import direction so this module adds all init functions. All init functions are
-* wrapped in another function before being run in a thread (i.e. run indirectly). Functions fail
-* either by throwing an exception or by returning a boolean. Functions may take parameters, e.g.
-* "out cleanupFunc[]". */
-module mde.setup.initFunctions;
-
-/+ unused
-import tango.util.log.Log : Log, Logger;
-static this() {
-    logger = Log.getLogger ("mde.setup.initFunctions");
-}
-private Logger logger;
-+/
-
-void setInitFailure () {    /// Call to indicate failure in an init function
-    initFailure = true;
-}
-
-/** Represents all functions to be called for a particular init stage. */
-struct InitStage
-{
-    struct InitFunction {
-        void delegate() func;       // the actual function
-        char[] name;                // it's name;
-    }
-    
-    /** Add a function to be called during this init stage.
-    *
-    * Called in order added when not threaded (reverse order for cleanup).
-    *
-    * Exceptions should never be thrown, since each function may run as a thread, and catching
-    * thread exceptions is not guaranteed to work. Log a message, call setFailure() and return
-    * instead. */
-    void addFunc (void delegate() f, char[] name) {
-        InitFunction s;
-        s.func = f;
-        s.name = name;
-        funcs ~= s;
-    }
-    void addFunc (void function() f, char[] name) { /// ditto
-        InitFunction s;
-        s.func.funcptr = f;
-        s.name = name;
-        funcs ~= s;
-    }
-    
-    InitFunction[] funcs = [];
-}
-
-InitStage init;     // all functions called during init (all should be thread-safe)
-//FIXME: implement:
-InitStage save;     // all functions to be called to save data (possible to run more than once)
-InitStage cleanup;  // all functions called during cleanup (all should be thread-safe)
-
-package:
-bool initFailure = false;   // set on failure (throwing through threads isn't a good idea)
--- a/mde/setup/paths.d	Sun Aug 31 15:59:17 2008 +0100
+++ b/mde/setup/paths.d	Thu Sep 11 11:33:51 2008 +0100
@@ -331,29 +331,29 @@
             for (uint i = 0; i < readersLen; ++i) readers[i].dataSecCreator = dsC;
         }
     
-    /** Get identifiers for all sections.
-     *
-     * Note: the identifiers from all sections in all files are just strung together, starting with
-     * the highest-priority file. */
-    ID[] getSectionNames () {
-        ID[] names;
-        for (int i = readersLen-1; i >= 0; --i) names ~= readers[i].getSectionNames;
-        return names;
-            }
-            void read () {                      /// Commence reading
-                for (uint i = 0; i < readersLen; ++i) readers[i].read();
-            }
-            void read (ID[] secSet) {           /// ditto
-                for (uint i = 0; i < readersLen; ++i) readers[i].read(secSet);
-            }
-            void read (View!(ID) secSet) {      /// ditto
-                for (uint i = 0; i < readersLen; ++i) readers[i].read(secSet);
-            }
+        /** Get identifiers for all sections.
+         *
+         * Note: the identifiers from all sections in all files are just strung together, starting with
+         * the highest-priority file. */
+        ID[] getSectionNames () {
+            ID[] names;
+            for (int i = readersLen-1; i >= 0; --i) names ~= readers[i].getSectionNames;
+            return names;
+        }
+        void read () {                      /// Commence reading
+            for (uint i = 0; i < readersLen; ++i) readers[i].read();
+        }
+        void read (ID[] secSet) {           /// ditto
+            for (uint i = 0; i < readersLen; ++i) readers[i].read(secSet);
+        }
+        void read (IContainer   !(ID) secSet) {      /// ditto
+            for (uint i = 0; i < readersLen; ++i) readers[i].read(secSet);
+        }
         
         private:
             IReader[MAX_PATHS] readers;
             ubyte readersLen = 0;
-    
+            
             PRIORITY rdOrder;
     }
 }
--- a/mde/setup/sdl.d	Sun Aug 31 15:59:17 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,195 +0,0 @@
-/* LICENSE BLOCK
-Part of mde: a Modular D game-oriented Engine
-Copyright © 2007-2008 Diggory Hardy
-
-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, see <http://www.gnu.org/licenses/>. */
-
-/** Just a temporary place to put SDL init stuff.
-*/
-module mde.setup.sdl;
-
-import mde.setup.initFunctions;
-import mde.input.joystick;
-import mde.lookup.Options;
-import mde.gl.draw;
-import imde = mde.imde;
-
-import tango.util.log.Log : Log, Logger;
-import tango.stdc.stringz;
-
-import derelict.sdl.sdl;
-import derelict.opengl.gl;	// for loading a later gl version
-import derelict.util.exception;
-
-private Logger logger;
-static this() {
-    logger = Log.getLogger ("mde.setup.sdl");
-    
-    init.addFunc (&initSdlAndGl, "initSdlAndGl");
-}
-
-private uint flags = 0;
-
-void initSdlAndGl() {   // init func
-    // Initialise SDL
-    if (SDL_Init (SDL_INIT_VIDEO | SDL_INIT_JOYSTICK /+| SDL_INIT_EVENTTHREAD+/)) {
-        logger.fatal ("SDL initialisation failed:");
-        char* msg = SDL_GetError ();
-        logger.fatal (msg ? fromStringz(msg) : "no reason available");
-        
-        setInitFailure ();
-        return;
-    }
-    
-    debug logger.trace ("SDL initialised");
-    
-    // Must be called after SDL has been initialised, so cannot be a separate Init function.
-    openJoysticks ();                   // after SDL init
-    cleanup.addFunc (&cleanupSDL, "cleanupSDL");
-    
-    setupWindow();
-}
-
-void setupWindow() {    // indirect init func (depends on initSdlAndGl)
-    // Window creation flags and size
-    flags = SDL_OPENGL;
-    if (vidOpts.hardware) flags |= SDL_HWSURFACE | SDL_DOUBLEBUF;
-    else flags |= SDL_SWSURFACE;
-    int w, h;
-    if (vidOpts.fullscreen) {
-        flags |= SDL_FULLSCREEN;
-        w = vidOpts.screenW;
-        h = vidOpts.screenH;
-    }
-    else {
-        if (vidOpts.resizable) flags |= SDL_RESIZABLE;
-        if (vidOpts.noFrame) flags |= SDL_NOFRAME;
-        w = vidOpts.windowW;
-        h = vidOpts.windowH;
-    }
-    
-    // OpenGL attributes
-    SDL_GL_SetAttribute(SDL_GL_RED_SIZE,    5);
-    SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE,  6);
-    SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE,   5);
-    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE,  16);
-    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER,1);
-    
-    // Open a window
-    debug logger.trace ("Opening a window (this can crash if the libraries are messed up)");
-    if (SDL_SetVideoMode (w, h, 32, flags) is null) {
-        logger.fatal ("Unable to set video mode:");
-        char* msg = SDL_GetError ();
-        logger.fatal (msg ? fromStringz(msg) : "no reason available");
-        
-        // Print a load of info:
-        logger.info ("Available video modes:");
-        SDL_Rect** modes = SDL_ListModes (null, SDL_FULLSCREEN);
-        if (modes is null) logger.info ("None!");
-        else if (modes is cast(SDL_Rect**) -1) logger.info ("All modes are available");
-        else {
-            for (uint i = 0; modes[i] !is null; ++i) {
-                logger.info ("\t{}x{}", modes[i].w, modes[i].h);
-            }
-        }
-    
-        SDL_VideoInfo* vi = SDL_GetVideoInfo ();
-        if (vi !is null) {
-            logger.info ("Video info:");
-            logger.info ("Hardware surface support: "~ (vi.flags & SDL_HWSURFACE ? "yes" : "no"));
-            logger.info ("Video memory: {}", vi.video_mem);
-    
-            if (vi.vfmt !is null) {
-                logger.info ("Best video mode:");
-                logger.info ("Bits per pixel: {}", vi.vfmt.BitsPerPixel);
-            }
-        }
-        
-        setInitFailure ();
-        return;
-    }
-    
-    /* Now (must be done after GL context is created) we can try to load later version.
-     * The initial loading provides opengl 1.1 features.
-     *
-     * 1.4 is now used for glBlendColor (coloured text).
-     *
-     * Currently the latest version used is 1.3; adjust this as necessary. However, before using
-     * features from any OpenGL version > 1.1 a check must be made on what was loaded by calling
-     * DerelictGL.availableVersion(). Note that availableVersion() could be used instead to load
-     * the highest supported version but this way we know what we're getting.
-     */
-    if (DerelictGL.availableVersion < GLVersion.Version13) {
-        logger.fatal ("Required at least OpenGL 1.3");
-        setInitFailure;
-        return;
-    }
-    /+try {
-        DerelictGL.loadVersions(GLVersion.Version14);
-    } catch (SharedLibProcLoadException e) {
-        logger.warn ("Loading OpenGL version 1.4 failed:");
-        logger.warn (e.msg);
-        
-        //NOTE: might be worth guaranteeing a minimal version to save later checks?
-        /+ Do this if you want the program to abort:
-        setInitFailure ();
-        return;
-        +/
-    }+/
-    
-    // OpenGL stuff:
-    glSetup();
-    setProjection (w, h);
-    
-    // Window-manager settings
-    SDL_WM_SetCaption (toStringz ("mde"), null);
-    // SDL_WM_GrabInput (use later)
-}
-
-void resizeWindow (int w, int h) {
-    if (vidOpts.fullscreen) {
-        vidOpts.set!(int) ("screenW", w);
-        vidOpts.set!(int) ("screenH", h);
-    } else {
-        vidOpts.set!(int) ("windowW", w);
-        vidOpts.set!(int) ("windowH", h);
-    }
-    
-    if (SDL_SetVideoMode (w, h, 32, flags) is null) {
-        logger.fatal ("Unable to reset video mode:");
-        char* msg = SDL_GetError ();
-        logger.fatal (msg ? fromStringz(msg) : "no reason available");
-        
-        imde.run = false;
-    }
-    
-    // Reset the projection and viewport
-    setProjection (w, h);
-}
-
-void cleanupSDL () {    // cleanup func
-    closeJoysticks();
-    debug logger.trace ("cleanupSDL - joysticks closed");
-    SDL_Quit();
-}
-
-
-/** All video options. */
-OptionsVideo vidOpts;
-class OptionsVideo : Options {
-    mixin (impl!("bool fullscreen,hardware,resizable,noFrame; int screenW,screenH,windowW,windowH;"));
-    
-    static this() {
-        vidOpts = new OptionsVideo;
-        Options.addOptionsClass (vidOpts, "video");
-    }
-}