changeset 48:a98ffb64f066

Implemented font rendering (grayscale only; i.e. non-LCD). FontTexture creates a texture and caches glyphs. Font supports multiple styles/faces, loaded from config file (should probably be loaded via Options instead). TextBlock cache for glyph placement within a string. committer: Diggory Hardy <diggory.hardy@gmail.com>
author Diggory Hardy <diggory.hardy@gmail.com>
date Sat, 31 May 2008 12:40:26 +0100
parents e0839643ff52
children bca7e2342d77
files codeDoc/gui/content.txt codeDoc/jobs.txt data/conf/fonts.mtt data/conf/options.mtt mde/gl/basic.d mde/gl/draw.d mde/gui/widget/Ifaces.d mde/gui/widget/miscWidgets.d mde/resource/FontTexture.d mde/resource/exception.d mde/resource/font.d mde/scheduler/init2.d mde/sdl.d
diffstat 13 files changed, 695 insertions(+), 125 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/codeDoc/gui/content.txt	Sat May 31 12:40:26 2008 +0100
@@ -0,0 +1,29 @@
+Content system
+
+Requirements for interface to engine:
+*   Variables can be linked to options (or get and set)
+*   Callbacks can be set for events: when variables are set and "click events" (e.g. buttons)
+
+Requirements for within-gui content handling:
+*   Services acting on content, e.g.:
+	Clipboard (copy/paste)
+	Spell-checker
+	Widget editor (for gui-editing if widgets are handled as content)
+
+Content-Options interaction:
+*   Combine the two?
+*   Or link them?
+	Content can be linked to individual options, setting them
+	Content lists can be linked to option lists
+
+Content-Gui interaction:
+*   Gui can get content
+*   Gui can set content
+	triggers content event callbacks
+*   Content can cause a re-get/re-draw of widgets using their content?
+*   Gui list-widgets can be associated with content lists
+
+Content system:
+*   Content variables/lists can be created by engine or gui (or loaded as generic options?)
+*   Use options-like approach with named-variable support plus generic variable support (if not named, revert to a dynamic array for storage)
+*   Support arrays and possibly other data structures
--- a/codeDoc/jobs.txt	Fri May 23 13:13:08 2008 +0100
+++ b/codeDoc/jobs.txt	Sat May 31 12:40:26 2008 +0100
@@ -3,9 +3,6 @@
 
 
 In progress:
-Implementing font rendering
-Reading ft tutorial
-Use cartesian coordinates? Or not?
 
 
 
@@ -54,3 +51,7 @@
 
 
 Done (for git log message):
+Implemented font rendering (grayscale only; i.e. non-LCD).
+FontTexture creates a texture and caches glyphs.
+Font supports multiple styles/faces, loaded from config file (should probably be loaded via Options instead).
+TextBlock cache for glyph placement within a string.
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/conf/fonts.mtt	Sat May 31 12:40:26 2008 +0100
@@ -0,0 +1,5 @@
+{MT01}
+<char[]|fallback="default">
+{default}
+<char[]|path="/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans.ttf">
+<int|size=16>
--- a/data/conf/options.mtt	Fri May 23 13:13:08 2008 +0100
+++ b/data/conf/options.mtt	Sat May 31 12:40:26 2008 +0100
@@ -9,7 +9,7 @@
 {video}
 <bool|noFrame=false>
 <bool|resizable=true>
-<bool|hardware=true>
+<bool|hardware=false>
 <bool|fullscreen=false>
 <int|screenW=1280>
 <int|windowW=1272>
--- a/mde/gl/basic.d	Fri May 23 13:13:08 2008 +0100
+++ b/mde/gl/basic.d	Sat May 31 12:40:26 2008 +0100
@@ -25,12 +25,23 @@
 //BEGIN GL & window setup
 void glSetup () {
     glDisable(GL_DEPTH_TEST);
-    glEnable (GL_TEXTURE_2D);
+    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);
+    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
+    glEnable(GL_TEXTURE_2D);
+    glShadeModel(GL_FLAT);
     
     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) {
@@ -55,6 +66,7 @@
     glColor3f (r, g, b);
 }
 void drawBox (int x, int y, int w, int h) {
+    glDisable(GL_TEXTURE_2D);
     glRecti(x, y+h, x+w, y);
 }
 //END Drawing utils
--- a/mde/gl/draw.d	Fri May 23 13:13:08 2008 +0100
+++ b/mde/gl/draw.d	Sat May 31 12:40:26 2008 +0100
@@ -26,6 +26,7 @@
 import tango.time.Time;     // TimeSpan (type only; unused)
 import tango.util.log.Log : Log, Logger;
 
+import mde.resource.font;
 private Logger logger;
 static this () {
     logger = Log.getLogger ("mde.gl.draw");
@@ -37,6 +38,7 @@
     glClear(GL_COLOR_BUFFER_BIT);
     
     gui.draw ();
+    FontStyle.drawTexture;
     
     GLenum err = glGetError();
     if (err != GL_NO_ERROR) {
--- a/mde/gui/widget/Ifaces.d	Fri May 23 13:13:08 2008 +0100
+++ b/mde/gui/widget/Ifaces.d	Sat May 31 12:40:26 2008 +0100
@@ -44,7 +44,6 @@
     widgetID addCreationData (IWidget widget);
     
     /** Returns the window's gui. */
-    //NOTE: was going to remove this, but it's used more than I thought
     IGui gui ();
     
     /** The widget/window needs redrawing. */
--- a/mde/gui/widget/miscWidgets.d	Fri May 23 13:13:08 2008 +0100
+++ b/mde/gui/widget/miscWidgets.d	Sat May 31 12:40:26 2008 +0100
@@ -114,10 +114,12 @@
     
     void draw () {
         super.draw();
-        if (font is null) font = Font.get("/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans.ttf");
-        font.drawStr (x,y, "Text Widget");
+        if (font is null) font = FontStyle.get("default");
+        font.textBlock (x,y, "|−|−| .,.,.", textCache);	// test new-lines and unicode characters
+        //old string: "Text Widget\nαβγ − ΑΒΓ"
     }
     
 protected:
-    static Font font;
+    TextBlock textCache;
+    static FontStyle font;
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/resource/FontTexture.d	Sat May 31 12:40:26 2008 +0100
@@ -0,0 +1,394 @@
+/* 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/>. */
+
+/** Font caching system.
+ *
+ * Three types of coordinates get used in the system: FreeType coordinates for each glyph, texture
+ * coordinates, and OpenGL's model/world coordinates (for rendering). The freetype and texture
+ * coords are cartesian (i.e. y increases upwards), although largely this is too abstract to
+ * matter. However, for the model/world coords, y increases downwards. */
+module mde.resource.FontTexture;
+
+import mde.resource.exception;
+
+import derelict.freetype.ft;
+import derelict.opengl.gl;
+
+import Utf = tango.text.convert.Utf;
+import tango.util.log.Log : Log, Logger;
+import tango.io.Stdout;
+
+private Logger logger;
+static this () {
+    logger = Log.getLogger ("mde.resource.FontTexture");
+}
+
+auto hinting = FT_LOAD_TARGET_NORMAL; //or FT_LOAD_TARGET_LCD (or others)
+//FIXME: allow setting lcd filtering
+auto lcdFilter = FT_LcdFilter.FT_LCD_FILTER_DEFAULT;	//altertives: NONE, LIGHT
+static const int dimW = 256, dimH = 256;	// Texture size
+const wFactor = 1f / dimW;
+const hFactor = 1f / dimH;
+
+/** A FontTexture is basically a cache of all font glyphs rendered so far.
+  *
+  * This class should be limited to code for rendering to (and otherwise handling) textures and
+  * rendering fonts to the screen.
+  *
+  * Technically, there's no reason it shouldn't be a static part of the FontStyle class. */
+class FontTexture
+{
+    this () {}
+    ~this () {
+        foreach (t; tex) {
+            glDeleteTextures (1, &(t.texID));
+        }
+    }
+    
+    // Call if font(s) have been changed and glyphs must be recached.
+    void clear () {
+        foreach (t; tex) {
+            glDeleteTextures (1, &(t.texID));
+        }
+        cachedGlyphs = null;
+        ++cacheVer;
+    }
+    
+    
+    /** Cache informatation for rendering a block of text.
+     *
+     * Recognises '\r', '\n' and "\r\n" as end-of-line markers. */
+    void updateCache (FT_Face face, char[] str, ref TextBlock cache)
+    {
+        debug scope (failure)
+                logger.warn ("updateCache failed");
+        
+        if (cache.cacheVer == cacheVer)	// Existing cache is up-to-date
+            return;
+        
+        cache.cacheVer = cacheVer;
+        
+        /* Convert the string to an array of character codes (which is equivalent to decoding UTF8
+        * to UTF32 since no character code is ever > dchar.max). */
+        static dchar[] chrs;	// keep memory for future calls (NOTE: change for threading)
+        chrs = Utf.toString32 (str, chrs);
+        
+        // Allocate space.
+        // Since end-of-line chars get excluded, will often be slightly larger than necessary.
+        cache.chars.length = chrs.length;
+        cache.chars.length = 0;
+        
+        int lineSep = face.size.metrics.height >> 6;
+        bool hasKerning = (FT_HAS_KERNING (face) != 0);
+        int y = 0;
+        CharCache cc;		// struct; reused for each character
+        
+        for (size_t i = 0; i < chrs.length; ++i)
+        {
+            // First, get maxmimal yMax for the current line.
+            int yMax = 0;	// Maximal glyph height above baseline.
+            for (size_t j = i; j < chrs.length; ++j)
+            {
+                if (chrs[j] == '\n' || chrs[j] == '\r')	// end of line
+                    break;
+                
+                GlyphAttribs* ga = chrs[j] in cachedGlyphs;
+                if (ga is null) {			// Not cached
+                    addGlyph (face, chrs[j]);		// so render it
+                    ga = chrs[j] in cachedGlyphs;	// get the ref of the copy we've stored
+                    assert (ga !is null, "ga is null: 1");
+                }
+                
+                if (ga.top > yMax)
+                    yMax = ga.top;
+            }
+            y += yMax;
+            
+            // Now for the current line:
+            int x = 0;	// x pos for next glyph
+            uint gi_prev = 0;	// previous glyph index (needed for kerning)
+            for (; i < chrs.length; ++i)
+            {
+                // If end-of-line, break to find yMax for next line.
+                if (chrs.length >= i+2 && chrs[i..i+2] == "\r\n"d) {
+                    ++i;
+                    break;
+                }
+                if (chrs[i] == '\n' || chrs[i] == '\r') {
+                    break;
+                }
+                
+                cc.ga = chrs[i] in cachedGlyphs;
+                assert (cc.ga !is null, "ga is null: 2");
+                
+                // Kerning
+                if (hasKerning && (gi_prev != 0)) {
+                    FT_Vector delta;
+                    FT_Get_Kerning (face, gi_prev, cc.ga.index, FT_Kerning_Mode.FT_KERNING_DEFAULT, &delta);
+                    x += delta.x >> 6;
+                }
+                
+                // ga.left component: adding this slightly improves glyph layout. Note that the
+                // left-most glyph on a line may not start right on the edge, but this looks best.
+                cc.xPos = x + cc.ga.left;
+                cc.yPos = y - cc.ga.top;
+                x += cc.ga.advanceX;
+                
+                cache.chars ~= cc;
+            }
+            // Now increment i and continue with the next line if there is one.
+            y += lineSep - yMax;
+        }
+    }
+    
+    /** Render a block of text using a cache. Updates the cache if necessary. */
+    void drawTextCache (FT_Face face, char[] str, ref TextBlock cache, int x, int y) {
+        updateCache (face, str, cache);	// update if necessary
+        debug scope (failure)
+                logger.warn ("drawTextCache failed");
+        
+        glEnable (GL_TEXTURE_2D);
+        glEnable(GL_BLEND);
+        glBlendFunc (GL_ONE, GL_ONE_MINUS_SRC_COLOR);
+        
+        foreach (chr; cache.chars) {
+            GlyphAttribs* ga = chr.ga;
+            
+            glBindTexture(GL_TEXTURE_2D, ga.texID);
+            
+            int x1 = x + chr.xPos;
+            int y1 = y + chr.yPos;
+            int x2 = x1 + ga.w;
+            int y2 = y1 + ga.h;
+            float tx1 = ga.x * wFactor;
+            float ty1 = ga.y * hFactor;
+            float tx2 = (ga.x + ga.w) * wFactor;
+            float ty2 = (ga.y + ga.h) * hFactor;
+           
+            glBegin (GL_QUADS);
+            glTexCoord2f (tx1, ty1);	glVertex2i (x1, y1);
+            glTexCoord2f (tx2, ty1);	glVertex2i (x2, y1);
+            glTexCoord2f (tx2, ty2);	glVertex2i (x2, y2);
+            glTexCoord2f (tx1, ty2);	glVertex2i (x1, y2);
+            glEnd ();
+        }
+        
+        glDisable(GL_BLEND);
+    }
+    
+    void addGlyph (FT_Face face, dchar chr) {
+        debug scope (failure)
+                logger.warn ("FontTexture.addGlyph failed!");
+        //Stdout ("Adding glyph ")(chr).newline;
+        auto gi = FT_Get_Char_Index (face, chr);
+        auto g = face.glyph;
+        
+        if (FT_Load_Glyph (face, gi, FT_LOAD_RENDER | hinting))
+            throw new fontGlyphException ("Unable to render glyph");
+        
+        auto b = g.bitmap;
+        if (b.pitch != b.width) {
+            char[128] tmp;
+            logger.warn (logger.format (tmp, "b.pitch is {}, b.width is {}", b.pitch, b.width));
+            //throw new fontGlyphException ("Unsupported freetype bitmap: b.pitch != b.width");
+        }
+        glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+        glPixelStorei (GL_UNPACK_ROW_LENGTH, b.pitch);
+        
+        GlyphAttribs ga;
+        ga.w		= b.width;
+        ga.h		= b.rows;
+        ga.left		= g.bitmap_left;
+        ga.top		= g.bitmap_top;
+        ga.advanceX	= g.advance.x >> 6;
+        ga.index	= gi;
+        Stdout ("Glyph left is: ")(ga.left).newline;
+        
+        foreach (ref t; tex) {
+            if (t.addGlyph (ga))
+                goto gotTexSpace;
+        }
+        // if here, no existing texture had the room for the glyph so create a new texture
+        // NOTE: check if using more than one texture impacts performance due to texture switching
+        logger.info ("Creating a font texture.");
+        tex ~= TexPacker.create();
+        assert (tex[$-1].addGlyph (ga), "Failed to fit glyph in a new texture but addGlyph didn't throw");
+        
+        gotTexSpace:
+        glBindTexture(GL_TEXTURE_2D, ga.texID);
+        GLenum format;
+        if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_GRAY && b.num_grays == 256)
+            format = GL_LUMINANCE;
+        else if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD ||
+                 b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD_V)
+            format = GL_RGB;
+        else
+            throw new fontGlyphException ("Unsupported freetype bitmap format");
+        
+        glTexSubImage2D(GL_TEXTURE_2D, 0,
+                        ga.x, ga.y,
+                        ga.w, ga.h,
+                        format, GL_UNSIGNED_BYTE,
+                        cast(void*) b.buffer);
+        
+        cachedGlyphs[chr] = ga;
+    }
+    
+    void drawTexture () {	// temp func
+        if (tex.length == 0) return;
+        glBindTexture(GL_TEXTURE_2D, tex[0].texID);
+        glEnable (GL_TEXTURE_2D);
+        glEnable(GL_BLEND);
+        glBlendFunc (GL_ONE, GL_ONE_MINUS_SRC_COLOR);
+        
+        glBegin (GL_QUADS);
+        glTexCoord2f (0f, 0f);	glVertex2i (0, 0);
+        glTexCoord2f (1f, 0f);	glVertex2i (dimW, 0);
+        glTexCoord2f (1f, 1f);	glVertex2i (dimW, dimH);
+        glTexCoord2f (0f, 1f);	glVertex2i (0, dimH);
+        glEnd ();
+        
+        glDisable(GL_BLEND);
+    }
+    
+private:
+    TexPacker[] tex;	// contains the gl texture id and packing data
+    
+    GlyphAttribs[dchar]	cachedGlyphs;
+    int cacheVer = 0;	// version of cache, used to make sure TextBlock caches are current.
+}
+
+// Use LinePacker for our texture packer:
+alias LinePacker TexPacker;
+
+/** Represents one gl texture; packs glyphs into lines. */
+struct LinePacker
+{
+    // create a new texture
+    static LinePacker create () {
+        LinePacker p;
+        //FIXME: check for error?
+        //glGenTextures (1, &(p.texID));
+        p.texID = 0;
+        //FIXME: why do I get a blank texture when using bind?
+        glBindTexture(GL_TEXTURE_2D, p.texID);
+        
+        // add a pretty background to the texture
+        static if (false) {
+            glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+            glPixelStorei (GL_UNPACK_ROW_LENGTH, 0);
+            ubyte[3][dimH][dimW] testTex;
+            for (size_t i = 0; i < dimW; ++i)
+                for (size_t j = 0; j < dimH; ++j)
+            {
+                testTex[i][j][0] = cast(ubyte) (i + j);
+                testTex[i][j][1] = cast(ubyte) i;
+                testTex[i][j][2] = cast(ubyte) j;
+            }
+            void* ptr = testTex.ptr;
+        } else
+            const void* ptr = null;
+        
+        // Create a texture without initialising values.
+        glTexImage2D(GL_TEXTURE_2D, 0, 3,
+                     dimW, dimH, 0,
+                     GL_RGB, GL_UNSIGNED_BYTE, ptr);
+        return p;
+    }
+    
+    /** Find space for a glyph of size attr.w, attr.h within the texture.
+     *
+     * Throws: fontGlyphException if glyph dimensions are larger than the texture.
+     *
+     * Returns false if unable to fit the glyph into the texture, true if successful. If
+     * successful, attr's x and y are set to suitible positions such that the rect given by attr's
+     * x, y, w & h is a valid subregion of the texture. */
+    bool addGlyph (ref GlyphAttribs attr) {
+        if (attr.w > dimW || attr.h > dimH)
+            throw new fontGlyphException ("Glyph too large to fit texture!");
+        
+        bool cantFitExtraLine = nextYPos + attr.h >= dimH;
+        foreach (ref line; lines) {
+            if (line.length + attr.w <= dimW &&		// if sufficient length and
+                line.height >= attr.h &&		// sufficient height and
+                (cantFitExtraLine ||			// either there's not room for another line
+                line.height <= attr.h * WASTE_H))	// or we're not wasting much vertical space
+            {						// then use this line
+                attr.x = line.length;
+                attr.y = line.yPos;
+                attr.texID = texID;
+                line.length += attr.w;
+                return true;
+            }
+        }
+        // If we didn't return, we didn't use an existing line.
+        if (cantFitExtraLine)				// run out of room
+            return false;
+        
+        // Still room: add a new line. The new line has the largest yPos (furthest down texture),
+        // but the lines array must remain ordered by line height (lowest to heighest).
+        Line line;
+        line.yPos = nextYPos;
+        line.height = attr.h * EXTRA_H;
+        line.length = attr.w;
+        size_t i = 0;
+        while (i < lines.length && lines[i].height < line.height) ++i;
+        lines = lines[0..i] ~ line ~ lines[i..$];	// keep lines sorted by height
+        nextYPos += line.height;
+        
+        attr.x = 0;	// first glyph in the line
+        attr.y = line.yPos;
+        attr.texID = texID;
+        return true;
+    }
+    
+    // Publically accessible data:
+    uint texID;				// OpenGL texture identifier (for BindTexture)
+    
+private:
+    const WASTE_H = 1.3;
+    const EXTRA_H = 1;	// can be float/double, just experimenting with 1
+    struct Line {
+        int yPos;	// y position (xPos is always 0)
+        int height;
+        int length;
+    }
+    Line[] lines;
+    int nextYPos = 0;	// y position for next created line (0 for first line)
+}
+
+struct GlyphAttribs {
+    int x, y;	// position within texture
+    int w, h;	// bitmap size
+    
+    int left, top;	// bitmap_left, bitmap_top fields
+    int advanceX;	// horizontal advance distance
+    uint index;		// glyph index (within face)
+    
+    uint texID;	// gl tex identifier
+}
+
+/** Cached information for drawing a block of text.
+ *
+ * Struct should be stored externally and updated via references. */
+struct TextBlock {
+    CharCache[] chars;	// All chars. They hold x & y pos. info, so don't need to know about lines.
+    int cacheVer = -1;	// this is checked on access, and must equal for cache to be valid.
+    int w, h;		/// Size of the block. Likely the only fields of use outside the library.
+}
+struct CharCache {
+    GlyphAttribs* ga;	// character
+    int xPos, yPos;	// x,y position
+}
--- a/mde/resource/exception.d	Fri May 23 13:13:08 2008 +0100
+++ b/mde/resource/exception.d	Sat May 31 12:40:26 2008 +0100
@@ -17,7 +17,7 @@
 module mde.resource.exception;
 import mde.exception;
 
-/// Thrown when initialising freetype fails.
+/// Thrown when initialisation fails
 class fontException : mdeException {
     char[] getSymbol () {
         return super.getSymbol ~ ".resource.font";
@@ -30,8 +30,15 @@
 
 /// Thrown when loading a freetype font fails.
 class fontLoadException : fontException {
+    this (char[] msg) {
+        super(msg);
+    }
+}
+
+/// Thrown when problems occur with glyphs (rendering, etc.)
+class fontGlyphException : fontException {
     char[] getSymbol () {
-        return super.getSymbol ~ ".resource.font";
+        return super.getSymbol ~ ".glyph";
     }
     
     this (char[] msg) {
--- a/mde/resource/font.d	Fri May 23 13:13:08 2008 +0100
+++ b/mde/resource/font.d	Sat May 31 12:40:26 2008 +0100
@@ -16,131 +16,243 @@
 /// Sets up freetype (in a basic way).
 module mde.resource.font;
 
+import mde.resource.FontTexture;
 import mde.resource.exception;
 
+import mde.mergetag.Reader;
+import mde.mergetag.DataSet;
+import mde.mergetag.exception;
+import mde.resource.paths;
+
 import derelict.freetype.ft;
 import derelict.opengl.gl;
 
+import tango.scrapple.text.convert.parseTo : parseTo;
 import tango.stdc.stringz;
+import Util = tango.text.Util;
 import tango.util.log.Log : Log, Logger;
 
+// "Publically import" this symbol:
+alias mde.resource.FontTexture.TextBlock TextBlock;
+
 private Logger logger;
 static this () {
     logger = Log.getLogger ("mde.resource.font");
 }
 
-/** Font class.
+/** FontStyle class.
  *
- * Particular to a font and size. (Maybe not size?)
+ * Particular to a font and size, and any other effects like bold/italic if ever implemented.
  * 
  * Note: it is not currently intended to be thread-safe. */
-class Font
+class FontStyle : IDataSection
 {
     //BEGIN Static: manager
     static {
-        /** Load the freetype library. */
-        void initialize () {
-            if (FT_Init_FreeType (&library))
-                throw new fontException ("error initialising the FreeType library");
+        debug void drawTexture() {
+            if (fontTex !is null)
+                fontTex.drawTexture;
         }
         
-        //FIXME: don't use GC for Font resources
+        /** Load the freetype library. */
+        void initialize () {
+            if (!confDir.exists (fileName))
+                throw new fontException ("No font settings file (fonts.[mtt|mtb])");
+            
+            if (FT_Init_FreeType (&library))
+                throw new fontException ("error initialising the FreeType library");
+            
+            // Check version
+            FT_Int maj, min, patch;
+            FT_Library_Version (library, &maj, &min, &patch);
+            if (maj != 2 || min != 3) {
+                char[128] tmp;
+                logger.warn (logger.format (tmp, "Using an untested FreeType version: {}.{}.{}", maj, min, patch));
+            }
+            
+            /* Load font settings
+             *
+             * Each mergetag section corresponds to a font; each is loaded whether used or not
+             * (however the actual font files are only loaded on use). A fallback id must be
+             * provided in the header which must match a loaded font name; if a non-existant font
+             * is requested a warning will be logged and this font returned. */
+            char[] fallbackName;
+            try {
+                IReader reader;
+                reader = confDir.makeMTReader (fileName, PRIORITY.LOW_HIGH, null, true);
+                reader.dataSecCreator = delegate IDataSection(ID id) {
+                    auto f = new FontStyle;
+                    fonts[id] = f;
+                    return f;
+                };
+                reader.read;
+                
+                // get fallback name
+                char[]* p = "fallback" in reader.dataset.header.Arg!(char[]).Arg;
+                if (p is null)
+                    throw new fontException ("No fallback font style specified");
+                fallbackName = *p;
+            }
+            catch (MTException e) {
+                throw new fontException ("Mergetag exception: "~e.msg);
+            }
+            
+            // Find the fallback
+            FontStyle* p = fallbackName in fonts;
+            if (p is null)
+                throw new fontException ("Fallback font style specified is not found");
+            fallback = *p;
+            // 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;
+        }
+        private const fileName = "fonts";
+        
+        //FIXME: don't use GC for FontStyle resources
         /** Cleanup: delete all fonts. */
         void cleanup () {
-            if (font)
-                delete font;
-            
             FT_Done_FreeType (library);
         }
         
-        /** Get a font.
-         *
-         * Later specify font/size.
-         *
-         * Throws:
-         *  fontLoadException when unable to load the font. */
-        Font get(char[] path) {
-            if (font is null) font = new Font(path);
-            return font;
+        /** Get a FontStyle instance, for a section in the fonts.mtt file.
+          *
+          * Also loads the font if it's not already loaded, so the first call may take some time.
+          *
+          * Uses fallback font-style if the desired style isn't known about or fails to load, so
+          * this function should never fail or throw, in theory (unless out of memory). The
+          * fallback should already be loaded. */
+        FontStyle get(char[] name) {
+            FontStyle* p = name in fonts;
+            if (p is null) {
+                logger.warn ("Font style "~name~" requested but not found; reverting to the fallback style.");
+                fonts[name] = fallback;	// set to prevent another warning getting logged
+                return fallback;
+            }
+            // Got it, but we need to make sure it's loaded:
+            try {
+                p.load;
+            } catch (Exception e) {
+                logger.warn ("Font style "~name~" failed to load; reverting to the fallback style.");
+                return fallback;
+            }
+            return *p;
         }
         
     private:
         FT_Library	library;
-        Font font;
+        FontTexture	fontTex;
+        FontStyle[ID]	fonts;		// all font styles known about; not necessarily loaded
+        FontStyle	fallback;	// used when requested font isn't in fonts
     }
     //END Static
     
+    this() {}
     
-    /** Load & cache a new font. */
-    this (char[] path)
+    //BEGIN Mergetag code
+    //NOTE: would it be better not to use a new mergetag file for this?
+    //FIXME: revise when gui can set options
+    void addTag (char[] tp, ID id, char[] dt) {
+        if (tp == "char[]") {
+            if (id == "path")
+                path = parseTo!(char[]) (dt);
+        }
+        else if (tp == "int") {
+            if (id == "size")
+                size = parseTo!(int) (dt);
+        }
+    }
+    void writeAll (ItemDelg) {}		// no writing the config for now
+    //END Mergetag code
+    
+    /** Load the font file.
+     *
+     * Even if the same font is used at multiple sizes, multiple copies of FT_Face are used.
+     * Sharing an FT_Face would require calling FT_Set_Pixel_Sizes each time a glyph is rendered or
+     * swapping the size information (face.size)? */
+    void load ()
     in {
         assert (library !is null, "font: library is null");
     } body {
         if (FT_New_Face (library, toStringz(path), 0, &face))
             throw new fontLoadException ("Unable to read font: "~path);
         
-        if (FT_Set_Pixel_Sizes (face, 0,16))
+        if (!FT_IS_SCALABLE (face))
+            throw new fontLoadException ("Currently no support for non-scalable fonts (which " ~
+                    path ~ " is). Please report if you want to see support.");
+        /* The following will need to be addressed when adding support for non-scalables:
+         *	Use of face.size.metrics.height property.
+         */
+        
+        if (FT_Set_Pixel_Sizes (face, 0,size))
             throw new fontLoadException ("Unable to set pixel size");
+        
+        // Create if necessary:
+        if (fontTex is null)
+            fontTex = new FontTexture;
+    }
+    
+    /** Update a TextBlock cache, as used by the textBlock function.
+     *
+     * The only use of this is to get the text block's size ahead of rendering, via TextBlock's w
+     * and h properties.
+     *
+     * This function will only actually update the cache if it is invalid, caused either by the
+     * font being changed or if cache.cacheVer < 0. */
+    void updateBlock (char[] str, ref TextBlock cache) {
+        fontTex.updateCache (face, str, cache);
     }
     
-    void drawStr (int x, int y, char[] str) {
-        FT_Vector pen = { x*64, y*64 };
-        auto g = face.glyph;
-        
-        FT_Matrix m;
-        m.xx = 0x10000;
-        m.xy = m.yx = 0;
-        m.yy = -0x10000;
-        
-        glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
-        
-        FT_Pos y_adj = 0;	// y adjustment (for height)
-        
-        FT_Bool useKerning = FT_HAS_KERNING (face);
-        FT_UInt previous = 0;
-        
-        foreach (chr; str) {
-            auto gi = FT_Get_Char_Index (face, chr);
-            
-            if (useKerning && previous && gi)
-            {
-                FT_Vector  delta;
-
-
-                FT_Get_Kerning (face, previous, gi, FT_Kerning_Mode.FT_KERNING_DEFAULT, &delta);
-
-                pen.x += delta.x;
-            }
-            
-            FT_Set_Transform(face, &m, &pen);
-            if (FT_Load_Glyph(face, gi, FT_LOAD_RENDER))
-                return;	// give up
-            
-            if (y_adj < g.metrics.height) y_adj = g.metrics.height;
-            
-            auto b = g.bitmap;
-            if (b.pixel_mode != FT_Pixel_Mode.FT_PIXEL_MODE_GRAY || b.num_grays != 256) {
-                char[128] tmp;
-                logger.warn (logger.format (tmp,"Unsupported freetype bitmap format: {}, {}", b.pixel_mode, b.num_grays));
-                return;
-            }
-            if (b.pitch != b.width)
-                logger.info ("b.pitch != b.width");
-            
-            //NOTE: y direction!
-            glRasterPos2i (g.bitmap_left,g.bitmap_top /+ (y_adj >> 6)+/);
-            glDrawPixels (b.width, b.rows, GL_LUMINANCE, GL_UNSIGNED_BYTE, cast(void*) b.buffer);
-            
-            pen.x += g.advance.x;
-            pen.y += g.advance.y;
-            previous = gi;
+    /** Draw a block of text (may inlcude new-lines).
+     *
+     * As a CPU-side code optimisation, store a TextBlock (unique to str) and pass a reference as
+     * the cache argument. This is the recommended method, although for one-time calls when you
+     * don't need to know the size, the other version of textBlock may be used.
+     * ---------------------------------
+     * char[] str;
+     * TextBlock strCache;
+     * textBlock (x, y, str, strCache);
+     * ---------------------------------
+     * The TextBlock cache will be updated as necessary. Besides the initial update, this will only
+     * be if the font changes, or it is manually invalidated. This can be done by setting the
+     * TextBlock's cacheVer property to -1, which should be done if str is changed.
+     *
+     * The TextBlock's w and h properties are set to the size (in pixels) of the text block; other
+     * than this cache only serves as a small optimisation. However, the only way to get the size
+     * of a text block is to use a TextBlock cache and update it, either with this function or with
+     * the updateBlock function. */
+    void textBlock (int x, int y, char[] str, ref TextBlock cache) {
+        try {
+            fontTex.drawTextCache (face, str, cache, x, y);
+        } catch (Exception e) {
+            logger.warn ("Exception while drawing text: "~e.msg);
         }
     }
+    /** ditto */
+    void textBlock (int x, int y, char[] str) {
+        try {
+            // Using the cache method for one-time use is slightly less than optimal, but doing so
+            // isn't really recommended anyway (and maintaining two versions of fontTex.drawText
+            // would be horrible).
+            TextBlock cache;
+            fontTex.drawTextCache (face, str, cache, x, y);
+        } catch (Exception e) {
+            logger.warn ("Exception while drawing text: "~e.msg);
+        }
+    }
+    
+    
+    /** The font-specified vertical distance between the baseline of consecutive lines. */
+    int getLineSeparation () {
+        return face.size.metrics.height >> 6;
+    }
     
     ~this () {
         FT_Done_Face (face);
     }
     
 private:
+    char[]	path;	// path to font file
+    int		size;	// font size
+    
     FT_Face	face;
 }
--- a/mde/scheduler/init2.d	Fri May 23 13:13:08 2008 +0100
+++ b/mde/scheduler/init2.d	Sat May 31 12:40:26 2008 +0100
@@ -86,7 +86,7 @@
 
 void initFreeType () {  // init func
     try {
-        font.Font.initialize;
+        font.FontStyle.initialize;
     } catch (Exception e) {
         logger.fatal ("initFreeType failed: " ~ e.msg);
         setInitFailure;
--- a/mde/sdl.d	Fri May 23 13:13:08 2008 +0100
+++ b/mde/sdl.d	Sat May 31 12:40:26 2008 +0100
@@ -27,6 +27,8 @@
 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() {
@@ -76,10 +78,10 @@
     }
     
     // OpenGL attributes
-    SDL_GL_SetAttribute(SDL_GL_RED_SIZE,    8);
-    SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE,  8);
-    SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE,   8);
-    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE,  24);
+    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
@@ -89,23 +91,54 @@
         char* msg = SDL_GetError ();
         logger.fatal (msg ? fromStringz(msg) : "no reason available");
         
+        // Print a load of info:
+        logger.info ("Available video modes:");
+        char[128] tmp;
+        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 (logger.format (tmp, "\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 (logger.format (tmp, "Video memory: {}", vi.video_mem));
+    
+            if (vi.vfmt !is null) {
+                logger.info ("Best video mode:");
+                logger.info (logger.format (tmp, "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.
-    /+ No later GL features are currently used.
+    /* Now (must be done after GL context is created) we can try to load later version.
+     * The initial loading provides opengl 1.1 features.
+     *
+     * 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.
+     */
     try {
-        DerelictGL.loadVersions(GLVersion.Version21);
-    } catch (DerelictException de) {
-        logger.fatal ("Loading OpenGL version > 1.1 failed:");
-        logger.fatal (de.msg);
+        DerelictGL.loadVersions(GLVersion.Version13);
+    } catch (SharedLibProcLoadException e) {
+        logger.warn ("Loading OpenGL version 1.3 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();
@@ -142,32 +175,6 @@
     SDL_Quit();
 }
 
-    /+ Load of info-printing stuff (currently doesn't have a use)
-    // Print a load of info:
-    logger.info ("Available video modes:");
-    char[128] tmp;
-    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 (logger.format (tmp, "\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 (logger.format (tmp, "Video memory: {}", vi.video_mem));
-    
-    if (vi.vfmt !is null) {
-    logger.info ("Best video mode:");
-    logger.info (logger.format (tmp, "Bits per pixel: {}", vi.vfmt.BitsPerPixel));
-    }
-    }
-    +/
-
 
 /** All video options. */
 OptionsVideo vidOpts;