Mercurial > projects > mde
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;