gems: TrueType VFS plugin

This commit introduces a VFS plugin that exposes the glyphs and
metadata of a TrueType font as a pseudo file system. The TTF font data
is obtained from the VFS. The resulting pseudo file system is a
directory that contains the files 'glyphs', 'baseline', 'max_width',
and 'max_height'.

The counter part of the plugin is the 'Vfs_font' class that implements
the 'Text_painter::Font' interface by accessing the pseudo file system
as provided by the TTF VFS plugin.

Fixes #2740
This commit is contained in:
Norman Feske 2018-03-26 17:16:47 +02:00 committed by Christian Helmuth
parent c5066a7317
commit 81e55e8901
9 changed files with 801 additions and 0 deletions

View File

@ -0,0 +1,270 @@
/*
* \brief Glyph cache
* \author Norman Feske
* \date 2018-03-27
*/
/*
* Copyright (C) 2018 Genode Labs GmbH
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
#ifndef _INCLUDE__GEMS__CACHED_FONT_T_
#define _INCLUDE__GEMS__CACHED_FONT_T_
#include <base/output.h>
#include <base/allocator.h>
#include <nitpicker_gfx/text_painter.h>
namespace Genode { class Cached_font; }
class Genode::Cached_font : public Text_painter::Font
{
public:
struct Stats
{
unsigned misses;
unsigned hits;
unsigned consumed_bytes;
void print(Output &out) const
{
Genode::print(out, "used: ", consumed_bytes/1024, " KiB, "
"hits: ", hits, ", misses: ", misses);
}
};
private:
typedef Text_painter::Area Area;
typedef Text_painter::Font Font;
typedef Text_painter::Glyph Glyph;
struct Time { unsigned value; };
Allocator &_alloc;
Font const &_font;
size_t const _limit;
Time mutable _now { 0 };
Stats mutable _stats { };
class Cached_glyph : Avl_node<Cached_glyph>
{
private:
friend class Avl_node<Cached_glyph>;
friend class Avl_tree<Cached_glyph>;
friend class Cached_font;
Codepoint const _codepoint;
Glyph const _glyph;
Time _last_used;
Glyph::Opacity _values[];
bool _higher(Codepoint const other) const
{
return _codepoint.value > other.value;
}
unsigned _importance(Time now) const
{
return now.value - _last_used.value;
}
public:
Cached_glyph(Codepoint c, Glyph const &glyph, Time now)
:
_codepoint(c),
_glyph({ .width = glyph.width,
.height = glyph.height,
.vpos = glyph.vpos,
.advance = glyph.advance,
.values = _values }),
_last_used(now)
{
for (unsigned i = 0; i < glyph.num_values(); i++)
_values[i] = glyph.values[i];
}
/**
* Avl_node interface
*/
bool higher(Cached_glyph *c) { return _higher(c->_codepoint); }
Cached_glyph *find_by_codepoint(Codepoint requested)
{
if (_codepoint.value == requested.value) return this;
Cached_glyph *c = Avl_node<Cached_glyph>::child(_higher(requested));
return c ? c->find_by_codepoint(requested) : nullptr;
}
Cached_glyph *find_least_recently_used(Time now)
{
Cached_glyph *result = this;
for (unsigned i = 0; i < 2; i++) {
Cached_glyph *c = Avl_node<Cached_glyph>::child(i);
if (c && c->_importance(now) > result->_importance(now))
result = c;
}
return result;
}
void mark_as_used(Time now) { _last_used = now; }
void apply(Font::Apply_fn const &fn) const { fn.apply(_glyph); }
};
Avl_tree<Cached_glyph> mutable _avl_tree { };
/**
* Size of one cache entry in bytes
*/
size_t const _alloc_size = sizeof(Cached_glyph)
+ 4*_font.bounding_box().count();
/**
* Add cache entry for the given glyph
*
* \throw Out_of_ram
* \throw Out_of_caps
*/
void _insert(Codepoint codepoint, Glyph const &glyph)
{
auto const cached_glyph_ptr = (Cached_glyph *)_alloc.alloc(_alloc_size);
_stats.consumed_bytes += _alloc_size;
memset(cached_glyph_ptr, 0, _alloc_size);
construct_at<Cached_glyph>(cached_glyph_ptr, codepoint, glyph, _now);
_avl_tree.insert(cached_glyph_ptr);
}
/**
* Evict glyph from cache
*/
void _remove(Cached_glyph &glyph)
{
_avl_tree.remove(&glyph);
glyph.~Cached_glyph();
_alloc.free(&glyph, _alloc_size);
_stats.consumed_bytes -= _alloc_size;
}
Cached_glyph *_find_by_codepoint(Codepoint codepoint)
{
if (!_avl_tree.first())
return nullptr;
return _avl_tree.first()->find_by_codepoint(codepoint);
}
/**
* Evice least recently used glyph from cache
*
* \return true if a glyph was released
*/
bool _remove_least_recently_used()
{
if (!_avl_tree.first())
return false;
Cached_glyph *glyph = _avl_tree.first()->find_least_recently_used(_now);
if (!glyph)
return false; /* this should never happen */
_remove(*glyph);
_stats.misses++;
return true;
}
void _remove_all()
{
while (Cached_glyph *glyph_ptr = _avl_tree.first())
_remove(*glyph_ptr);
}
public:
struct Limit { size_t value; };
/**
* Constructor
*
* \param alloc backing store for cached glyphs
* \param font original (uncached) font
* \param limit maximum cache size in bytes
*/
Cached_font(Allocator &alloc, Font const &font, Limit limit)
:
_alloc(alloc), _font(font), _limit(limit.value)
{ }
~Cached_font() { _remove_all(); }
Stats stats() const { return _stats; }
void _apply_glyph(Codepoint c, Apply_fn const &fn) const override
{
_now.value++;
/*
* Try to lookup glyph from the cache. If it is missing, fill cache
* with requested glyph and repeat the lookup. When under memory
* pressure, flush least recently used glyphs from cache.
*
* Even though '_apply_glyph' is a const method, the internal cache
* and stats must of course be mutated. Hence the 'const_cast'.
*/
Cached_font &mutable_this = const_cast<Cached_font &>(*this);
/* retry once after handling a cache miss */
for (int i = 0; i < 2; i++) {
if (Cached_glyph *glyph_ptr = mutable_this._find_by_codepoint(c)) {
glyph_ptr->apply(fn);
glyph_ptr->mark_as_used(_now);
_stats.hits += (i == 0);
return;
}
while (_stats.consumed_bytes + _alloc_size > _limit)
if (!mutable_this._remove_least_recently_used())
break;
_font.apply_glyph(c, [&] (Glyph const &glyph) {
mutable_this._insert(c, glyph); });
}
}
Advance_info advance_info(Codepoint c) const override
{
unsigned width = 0;
Text_painter::Fixpoint_number advance { 0 };
/* go through the '_apply_glyph' cache-fill mechanism */
Font::apply_glyph(c, [&] (Glyph const &glyph) {
width = glyph.width, advance = glyph.advance; });
return Advance_info { .width = width, .advance = advance };
}
unsigned baseline() const override { return _font.baseline(); }
Area bounding_box() const override { return _font.bounding_box(); }
};
#endif /* _INCLUDE__GEMS__CACHED_FONT_T_ */

View File

@ -0,0 +1,169 @@
/*
* \brief Implementation of 'Text_painter::Font' for VFS-mounted fonts
* \author Norman Feske
* \date 2018-03-26
*/
/*
* Copyright (C) 2018 Genode Labs GmbH
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
#ifndef _INCLUDE__GEMS__VFS_FONT_T_
#define _INCLUDE__GEMS__VFS_FONT_T_
#include <gems/vfs.h>
#include <nitpicker_gfx/text_painter.h>
namespace Genode { class Vfs_font; }
class Genode::Vfs_font : public Text_painter::Font
{
public:
typedef Glyph_painter::Glyph Glyph;
static constexpr Vfs::file_size GLYPH_SLOT_BYTES = 64*1024;
class Glyph_header
{
private:
uint8_t _width = 0;
uint8_t _height = 0;
uint8_t _vpos = 0;
int8_t _advance_decimal = 0;
uint8_t _advance_fractional = 0;
uint8_t _reserved[3] { };
Glyph::Opacity _values[];
float _advance() const
{
float value = 256.0*_advance_decimal + _advance_fractional;
return value/256;
}
public:
Glyph_header(Glyph const &glyph)
:
_width ((uint8_t)min(255U, glyph.width)),
_height((uint8_t)min(255U, glyph.height)),
_vpos ((uint8_t)min(255U, glyph.vpos)),
_advance_decimal((int8_t)max(-127, min(127, glyph.advance.decimal()))),
_advance_fractional((uint8_t)glyph.advance.value & 0xff)
{ }
Glyph_header() { }
Glyph glyph() const { return Glyph { .width = _width,
.height = _height,
.vpos = _vpos,
.advance = _advance(),
.values = _values }; }
} __attribute__((packed));
private:
typedef Text_painter::Codepoint Codepoint;
typedef Text_painter::Area Area;
typedef Directory::Path Path;
Directory const _font_dir;
unsigned const _baseline;
Area const _bounding_box;
struct Glyph_buffer
{
Allocator &_alloc;
unsigned const num_bytes;
Glyph_header &header;
Glyph_buffer(Allocator &alloc, Area size)
:
_alloc(alloc), num_bytes(sizeof(Glyph_header) + size.count()*4),
header(*(Glyph_header *)alloc.alloc(num_bytes))
{ }
~Glyph_buffer() { _alloc.free(&header, num_bytes); }
char *ptr() { return (char *)&header; }
};
Glyph_buffer mutable _buffer;
Readonly_file _glyphs_file;
template <typename T, unsigned MAX_LEN = 128>
static T _value_from_file(Directory const &dir, Path const &path,
T const &default_value)
{
T result = default_value;
try {
Readonly_file const file(dir, path);
char buf[MAX_LEN + 1] { };
if (file.read(buf, sizeof(buf)) <= MAX_LEN)
if (ascii_to(buf, result))
return result;
}
catch (Readonly_file::Open_failed) { }
return default_value;
}
static Readonly_file::At _file_pos(Codepoint c)
{
return Readonly_file::At{(Vfs::file_size)c.value*GLYPH_SLOT_BYTES};
}
public:
struct Unavailable : Exception { };
/**
* Constructor
*
* \param alloc allocator for glyph buffer
* \param dir directory
* \param path path to font
*
* \throw Unavailable unable to obtain font data
*/
Vfs_font(Allocator &alloc, Directory const &dir, Path const &path)
:
_font_dir(dir, path),
_baseline(_value_from_file(_font_dir, "baseline", 0U)),
_bounding_box(_value_from_file(_font_dir, "max_width", 0U),
_value_from_file(_font_dir, "max_height", 0U)),
_buffer(alloc, _bounding_box),
_glyphs_file(_font_dir, "glyphs")
{ }
void _apply_glyph(Codepoint c, Apply_fn const &fn) const override
{
_glyphs_file.read(_file_pos(c), _buffer.ptr(), _buffer.num_bytes);
fn.apply(_buffer.header.glyph());
}
Advance_info advance_info(Codepoint c) const override
{
_glyphs_file.read(_file_pos(c), _buffer.ptr(), sizeof(Glyph_header));
Glyph const glyph = _buffer.header.glyph();
return Advance_info { .width = glyph.width, .advance = glyph.advance };
}
unsigned baseline() const override { return _baseline; }
Area bounding_box() const override { return _bounding_box; }
};
#endif /* _INCLUDE__GEMS__TTF_FONT_T_ */

View File

@ -0,0 +1,9 @@
SRC_CC = vfs.cc
INC_DIR += $(REP_DIR)/src/lib/vfs/ttf
LIBS += ttf_font
vpath %.cc $(REP_DIR)/src/lib/vfs/ttf
SHARED_LIB = yes

View File

@ -0,0 +1,16 @@
MIRROR_FROM_REP_DIR := lib/mk/vfs_ttf.mk lib/mk/ttf_font.mk \
src/lib/vfs/ttf src/lib/ttf_font
STB_PORT_DIR := $(call port_dir,$(GENODE_DIR)/repos/libports/ports/stb)
content: $(MIRROR_FROM_REP_DIR) include/stb_truetype.h LICENSE
include/stb_truetype.h:
mkdir -p $(dir $@)
cp -r $(STB_PORT_DIR)/include/stb_truetype.h $@
$(MIRROR_FROM_REP_DIR):
$(mirror_from_rep_dir)
LICENSE:
cp $(GENODE_DIR)/LICENSE $@

View File

@ -0,0 +1 @@
2018-03-30 89b39a8703dbb8b098068d2ec4f73b4705debb44

View File

@ -0,0 +1,7 @@
base
os
so
libc
vfs
gems
nitpicker_gfx

View File

@ -0,0 +1,154 @@
/*
* \brief Glyphs file system
* \author Norman Feske
* \date 2018-03-26
*/
/*
* Copyright (C) 2018 Genode Labs GmbH
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
#ifndef _GLYPHS_FILE_SYSTEM_H_
#define _GLYPHS_FILE_SYSTEM_H_
/* Genode includes */
#include <vfs/single_file_system.h>
#include <nitpicker_gfx/text_painter.h>
/* gems includes */
#include <gems/vfs_font.h>
namespace Vfs {
using namespace Genode;
class Glyphs_file_system;
typedef Text_painter::Font Font;
typedef Vfs_font::Glyph_header Glyph_header;
}
class Vfs::Glyphs_file_system : public Vfs::Single_file_system
{
private:
static constexpr unsigned UNICODE_MAX = 0x10ffff;
static constexpr file_size FILE_SIZE = Vfs_font::GLYPH_SLOT_BYTES*(UNICODE_MAX + 1);
Font const &_font;
struct Vfs_handle : Single_vfs_handle
{
Font const &_font;
Vfs_handle(Directory_service &ds,
File_io_service &fs,
Allocator &alloc,
Font const &font)
:
Single_vfs_handle(ds, fs, alloc, 0), _font(font)
{ }
Read_result read(char *dst, file_size count,
file_size &out_count) override
{
out_count = 0;
if (seek() > FILE_SIZE)
return READ_ERR_INVALID;
Codepoint const codepoint { (uint32_t)(seek() / Vfs_font::GLYPH_SLOT_BYTES) };
file_size byte_offset = seek() % Vfs_font::GLYPH_SLOT_BYTES;
_font.apply_glyph(codepoint, [&] (Glyph_painter::Glyph const &glyph) {
if (byte_offset < sizeof(Glyph_header)) {
Glyph_header const header(glyph);
char const * const src = (char const *)&header + byte_offset;
size_t const len = min(sizeof(header) - byte_offset, count);
memcpy(dst, src, len);
dst += len;
byte_offset += len;
count -= len;
out_count += len;
}
/*
* Given that 'byte_offset' is at least 'sizeof(header)',
* continue working with 'alpha_offset', which is the first
* offset of interest within the array of alpha values.
*/
size_t const alpha_offset = byte_offset - sizeof(Glyph_header);
size_t const alpha_values_len = 4*glyph.width*glyph.height;
if (alpha_offset < alpha_values_len) {
char const * const src = (char const *)glyph.values + alpha_offset;
size_t const len = min(alpha_values_len - alpha_offset, count);
memcpy(dst, src, len);
out_count += len;
}
});
return READ_OK;
}
Write_result write(char const *, file_size, file_size &) override
{
return WRITE_ERR_IO;
}
bool read_ready() override { return true; }
};
public:
Glyphs_file_system(Font const &font)
:
Single_file_system(NODE_TYPE_CHAR_DEVICE, type(), Xml_node("<glyphs/>")),
_font(font)
{ }
static char const *type_name() { return "glyphs"; }
char const *type() override { return type_name(); }
/*********************************
** Directory-service interface **
*********************************/
Open_result open(char const *path, unsigned,
Vfs::Vfs_handle **out_handle,
Allocator &alloc) override
{
if (!_single_file(path))
return OPEN_ERR_UNACCESSIBLE;
try {
*out_handle = new (alloc)
Vfs_handle(*this, *this, alloc, _font);
return OPEN_OK;
}
catch (Genode::Out_of_ram) { return OPEN_ERR_OUT_OF_RAM; }
catch (Genode::Out_of_caps) { return OPEN_ERR_OUT_OF_CAPS; }
}
Stat_result stat(char const *path, Stat &out) override
{
Stat_result result = Single_file_system::stat(path, out);
out.mode |= 0444;
out.size = FILE_SIZE;
return result;
}
};
#endif /* _GLYPHS_FILE_SYSTEM_H_ */

View File

@ -0,0 +1,2 @@
TARGET = dummy-vfs_ttf
LIBS = vfs_ttf

View File

@ -0,0 +1,173 @@
/*
* \brief Truetype font file system
* \author Norman Feske
* \date 2018-03-07
*/
/*
* Copyright (C) 2018 Genode Labs GmbH
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
/* Genode includes */
#include <vfs/dir_file_system.h>
#include <vfs/readonly_value_file_system.h>
/* gems includes */
#include <gems/ttf_font.h>
#include <gems/vfs.h>
#include <gems/cached_font.h>
/* local includes */
#include <glyphs_file_system.h>
namespace Vfs_ttf {
using namespace Vfs;
using namespace Genode;
class Font_from_file;
class Local_factory;
class File_system;
struct Dummy_io_response_handler : Vfs::Io_response_handler
{
void handle_io_response(Vfs::Vfs_handle::Context *) override { };
};
typedef Text_painter::Font Font;
}
struct Vfs_ttf::Font_from_file
{
typedef Directory::Path Path;
Directory const _dir;
File_content const _content;
Constructible<Ttf_font const> _font { };
/*
* Each slot of the glyphs file is 64 KiB, which limits the maximum glyph
* size to 128x128. We cap the size at 100px to prevent cut-off glyphs.
*/
static constexpr float MAX_SIZE_PX = 100.0;
Font_from_file(Vfs::File_system &root, Entrypoint &ep, Allocator &alloc,
Path const &file_path, float px)
:
_dir(root, ep, alloc),
_content(alloc, _dir, file_path, File_content::Limit{10*1024*1024})
{
_content.bytes([&] (char const *ptr, size_t) {
_font.construct(alloc, ptr, min(px, MAX_SIZE_PX)); });
}
Font const &font() const { return *_font; }
};
struct Vfs_ttf::Local_factory : File_system_factory
{
Font_from_file _font;
Cached_font::Limit _cache_limit;
Cached_font _cached_font;
Glyphs_file_system _glyphs_fs;
Readonly_value_file_system<unsigned> _baseline_fs;
Readonly_value_file_system<unsigned> _max_width_fs;
Readonly_value_file_system<unsigned> _max_height_fs;
Local_factory(Env &env, Allocator &alloc, Xml_node node,
Vfs::File_system &root_dir)
:
_font(root_dir, env.ep(), alloc,
node.attribute_value("path", Directory::Path()),
node.attribute_value("size_px", 16.0)),
_cache_limit({node.attribute_value("cache", Number_of_bytes())}),
_cached_font(alloc, _font.font(), _cache_limit),
_glyphs_fs (_cached_font),
_baseline_fs ("baseline", _font.font().baseline()),
_max_width_fs ("max_width", _font.font().bounding_box().w()),
_max_height_fs("max_height", _font.font().bounding_box().h())
{ }
Vfs::File_system *create(Env &, Allocator &, Xml_node node,
Io_response_handler &, Vfs::File_system &) override
{
if (node.has_type(Glyphs_file_system::type_name()))
return &_glyphs_fs;
if (node.has_type(Readonly_value_file_system<unsigned>::type_name()))
return _baseline_fs.matches(node) ? &_baseline_fs
: _max_width_fs.matches(node) ? &_max_width_fs
: _max_height_fs.matches(node) ? &_max_height_fs
: nullptr;
return nullptr;
}
};
class Vfs_ttf::File_system : private Local_factory,
private Dummy_io_response_handler,
public Vfs::Dir_file_system
{
private:
typedef String<200> Config;
static Config _config(Xml_node node)
{
char buf[Config::capacity()] { };
Xml_generator xml(buf, sizeof(buf), "dir", [&] () {
typedef String<64> Name;
xml.attribute("name", node.attribute_value("name", Name()));
xml.node("glyphs", [&] () { });
xml.node("readonly_value", [&] () { xml.attribute("name", "baseline"); });
xml.node("readonly_value", [&] () { xml.attribute("name", "max_width"); });
xml.node("readonly_value", [&] () { xml.attribute("name", "max_height"); });
});
return Config(Cstring(buf));
}
public:
File_system(Env &env, Allocator &alloc, Xml_node node,
Vfs::File_system &root_dir)
:
Local_factory(env, alloc, node, root_dir),
Vfs::Dir_file_system(env, alloc,
Xml_node(_config(node).string()),
*this, *this, root_dir)
{ }
char const *type() override { return "ttf"; }
};
/**************************
** VFS plugin interface **
**************************/
extern "C" Vfs::File_system_factory *vfs_file_system_factory(void)
{
struct Factory : Vfs::File_system_factory
{
Vfs::File_system *create(Genode::Env &env, Genode::Allocator &alloc,
Genode::Xml_node node,
Vfs::Io_response_handler &,
Vfs::File_system &root_dir) override
{
try {
return new (alloc) Vfs_ttf::File_system(env, alloc, node, root_dir); }
catch (...) { }
return nullptr;
}
};
static Factory factory;
return &factory;
}