view mde/font/FontTexture.d @ 137:9f035cd139c6

BIG commit. Major change: old Options class is gone, all content values are loaded and saved automatically. All options updated to reflect this, some changed. Content restrutured a lot: New IContent module, Content module includes more functionality. New ContentLoader module to manage content loading/saving/translation. Translation module moved to content dir and cut down to reflect current usage. File format unchanged except renames: FontOptions -> Font, VideoOptions -> Screen. Font render mode and LCD filter options are now enums. GUI loading needs to create content (and set type for enums), but doesn't save/load value. Some setup of mainSchedule moved to mde.mainLoop. Content callbacks are called on content change now. ContentLists are set up implicitly from content symbols. Not as fast but much easier! Bug-fix in the new MTTagReader. Renamed MT *Reader maker functions to avoid confusion in paths.d. New mde.setup.logger module to allow logger setup before any other module's static this().
author Diggory Hardy <diggory.hardy@gmail.com>
date Sat, 07 Feb 2009 12:46:03 +0000
parents ee209602770d
children e45226d3deae
line wrap: on
line source

/* 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.
 *
 * This module also serves as the internals to the font module and shouldn't be used except through
 * the font module. The two modules could be combined, at a cost to readability.
 *
 * 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.font.FontTexture;

import mde.types.Colour;
import mde.content.AStringContent;
import mde.font.exception;

import derelict.freetype.ft;
import derelict.opengl.gl;

import Utf = tango.text.convert.Utf;
import tango.util.log.Log : Log, Logger;

private Logger logger;
static this () {
    logger = Log.getLogger ("mde.font.FontTexture");
}

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
{
    // 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.error ("updateCache failed");
        
        if (cache.cacheVer == cacheVer)	// Existing cache is up-to-date
            return;
        
        cache.cacheVer = cacheVer;
	cache.w = cache.h = 0;		// reset
        
        /* 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 = face.size.metrics.ascender >> 6;
        CharCache cc;		// struct; reused for each character
        
        for (size_t i = 0; i < chrs.length; ++i)
        {
            // For each 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;
                if (cc.ga is null) {                    // Not cached
                    addGlyph (face, chrs[i]);           // so render it
                    cc.ga = chrs[i] in cachedGlyphs;    // get the ref of the copy we've stored
                    assert (cc.ga !is null, "Unable to cache glyph!");
                }
                
                // 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;
                
                // Update rect total size. Top and left coords should be zero, so make width and
                // height maximal x and y coordinates.
                if (cc.xPos + cc.ga.w > cache.w)
                    cache.w = cc.xPos + cc.ga.w;
                /+if (cc.yPos + cc.ga.h > cache.h)
                    cache.h = cc.yPos + cc.ga.h;+/
            }
            // Now increment i and continue with the next line if there is one.
            y += lineSep;
            cache.h += lineSep;
        }
    }
    
    /** Render a block of text using a cache. Updates the cache if necessary.
     *
     * Params:
     *  face =	Current typeface pointer; must be passed from font.d (only needed if the cache is
     *  	invalid)
     *  str =	Text to render (only needed if the cache is invalid)
     *  cache =	Cache used to speed up CPU-side rendering code
     *  x =	Smaller x-coordinate of position
     *  y =	Smaller y-coordinate of position
     *  col =	Text colour (note: currently limited to black or white)
     *  index =	Index of edit position, or size_t.max if no idet position. */
    void drawCache (FT_Face face, char[] str, ref TextBlock cache, int x, int y, Colour col, size_t index) {
        updateCache (face, str, cache);	// update if necessary
        debug scope (failure)
                logger.error ("drawTextCache failed");
        
        // opaque (GL_DECAL would be equivalent)
        glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
        
        drawCacheImpl (face, cache, x, y, col, index);
    }
    /** A variation of drawCache, for transparent text.
     *
     * Instead of passing the alpha value(s) as arguments, set the openGL colour prior to calling:
     * ---
     * glColor3f (.5f, .5f, .5f);	// set alpha to half
     * drawCacheA (face, ...);
     * 
     * glColor3ub (0, 255, 127);	// alpha 0 for red, 1 for green, half for blue
     * drawCacheA (face, ...);
     * ---
     *
     * The overhead of the transparency is minimal. */
    void drawCacheA (FT_Face face, char[] str, ref TextBlock cache, int x, int y, Colour col, size_t index) {
        updateCache (face, str, cache);	// update if necessary
        debug scope (failure)
                logger.error ("drawTextCache failed");
        
        // transparency alpha
        // alpha is current colour, per component
        glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
        
	drawCacheImpl (face, cache, x, y, col, index);
    }
    
    private void drawCacheImpl (FT_Face face, ref TextBlock cache, int x, int y, Colour col, size_t index) {
        if (DerelictGL.availableVersion() >= GLVersion.Version14) {
            glBlendFunc (GL_CONSTANT_COLOR, GL_ONE_MINUS_SRC_COLOR);
            glBlendColor(col.r, col.g, col.b, 1f);	// text colour
        } else
            glBlendFunc (col.nearestGLConst, GL_ONE_MINUS_SRC_COLOR);
        
        glEnable (GL_TEXTURE_2D);
        glEnable(GL_BLEND);
        
        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_TEXTURE_2D);
        glDisable(GL_BLEND);
	
	if (index <= cache.chars.length) {	// draw edit position
	    int x1;
	    if (index == 0)
		x1 = x + 1;
	    else {
		auto c = cache.chars[index-1];
		x1 = x + c.xPos + c.ga.advanceX;
	    }
	    glColor3f(col.r, col.g, col.b);
	    glRecti(x1 - 1, y, x1, y + (face.size.metrics.height >> 6));
	}
    }
    
    void addGlyph (FT_Face face, dchar chr) {
        debug scope (failure)
                logger.error ("FontTexture.addGlyph failed!");
        
        auto gi = FT_Get_Char_Index (face, chr);
        auto g = face.glyph;
        
        if (FT_Load_Glyph (face, gi, FT_LOAD_RENDER | fontOpts.modeFlag))
            throw new fontGlyphException ("Unable to render glyph");
        
        auto b = g.bitmap;
        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;
        if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD)
            ga.w /= 3;
        if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD_V)
            ga.h /= 3;
        
        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
        debug logger.trace ("Creating a new 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;
        ubyte[] buffer;	// use our own pointer, since for LCD modes we need to perform a conversion
        if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_GRAY && b.num_grays == 256) {
            assert (b.pitch == b.width, "Have assumed b.pitch == b.width for gray glyphs.");
            buffer = b.buffer[0..b.pitch*b.rows];
            format = GL_LUMINANCE;
        } else if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD) {
            // NOTE: Can't seem to get OpenGL to read freetype's RGB buffers properly, so convent.
            /* NOTE: Sub-pixel rendering probably also needs filtering. I haven't tried, since it's
             * disabled in my build of the library. For a tutorial on the filtering, see:
             * http://dmedia.dprogramming.com/?n=Tutorials.TextRendering1 */
            buffer = new ubyte[b.width*b.rows];
            for (uint i = 0; i < b.rows; ++i)
                for (uint j = 0; j < b.width; j+= 3)
            {
                buffer[i*b.width + j + 0] = b.buffer[i*b.pitch + j + 0];
                buffer[i*b.width + j + 1] = b.buffer[i*b.pitch + j + 1];
                buffer[i*b.width + j + 2] = b.buffer[i*b.pitch + j + 2];
            }
            
            format = (fontOpts.mode() >= 4) ? GL_BGR : GL_RGB;
        } else if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD_V) {
            // NOTE: Notes above apply. But in this case converting the buffers seems essential.
            buffer = new ubyte[b.width*b.rows];
            for (uint i = 0; i < b.rows; ++i)
                for (uint j = 0; j < b.width; ++j)
            {
                // i/3 is the "real" row, b.width*3 is our width (with subpixels), j is column,
                // i%3 is sub-pixel (R/G/B). i/3*3 necessary to round to multiple of 3
                buffer[i/3*b.width*3 + 3*j + i%3] = b.buffer[i*b.pitch + j];
            }
            
            format = (fontOpts.mode() >= 4) ? GL_BGR : 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*) buffer.ptr);
        
        cachedGlyphs[chr] = ga;
    }
    
    // Draw the first glyph cache texture in the upper-left corner of the screen.
    debug (drawGlyphCache) void drawTexture () {
        if (tex.length == 0) return;
        glEnable (GL_TEXTURE_2D);
        glBindTexture(GL_TEXTURE_2D, tex[0].texID);
        glEnable(GL_BLEND);
        glBlendFunc (GL_ONE, GL_ONE_MINUS_SRC_COLOR);
        float[4] Cc = [ 1.0f, 1f, 1f, 1f ];
        glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, Cc.ptr);
        
        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_TEXTURE_2D);
        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 (gl): why do I get a blank texture when binding to non-0?
        //glGenTextures (1, &(p.texID));
        p.texID = 0;
        
        // add a pretty background to the texture
        debug (drawGlyphCache) {
            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.
        glBindTexture(GL_TEXTURE_2D, p.texID);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB,
                     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!");
        
        attr.texID = texID;		// Set now. Possibly reset if new texture is needed.
        if (attr.w == 0) return true;	// 0 sized glyph; x and y are unimportant.
        
        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;
        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 fontOpts {
static:
    EnumContent mode;
    int modeFlag;
    /* lcdFilter should come from enum FT_LcdFilter:
    * FT_LCD_FILTER_NONE (0), FT_LCD_FILTER_DEFAULT (1), FT_LCD_FILTER_LIGHT (2) */
    EnumContent lcdFilter;
    IntContent defaultSize;
    StringContent defaultFont;
    
    static this() {
        mode = new EnumContent ("Font.mode", ["normal"[],"light","lcd","lcd_v","lcd_bgr","lcd_bgr_v"]);
        lcdFilter = new EnumContent ("Font.lcdFilter", ["none"[],"default","light"]);
        defaultSize = new IntContent ("Font.defaultSize");
        defaultFont = new StringContent ("Font.defaultFont");
        mode.addCallback (delegate void(Content) {
           /* modeFlag should have one of the following values:
            * FT_LOAD_TARGET_NORMAL    (0x00000)
            * FT_LOAD_TARGET_LIGHT     (0x10000)
            * FT_LOAD_TARGET_LCD       (0x30000)
            * FT_LOAD_TARGET_LCD_V     (0x40000)
            * The mode FT_LOAD_TARGET_MONO (0x20000) is unsupported. */
            modeFlag = fontOpts.mode();
            if (modeFlag == 2 || modeFlag == 4) modeFlag = 0x30000;
            else if (modeFlag == 3 || modeFlag == 5) modeFlag = 0x40000;
            else modeFlag <<= 16;
        });
    }
}

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;	/// Checked on access; must equalFontTexture.cacheVer or the cache is rebuilt.
    int w, h;		/// Size of the block.
}
struct CharCache {
    GlyphAttribs* ga;	/// The character
    int xPos, yPos;	/// Character's x,y position
}