THIS IS A DRAFT - NOT FOR DISTRIBUTION
Introduction :
This document describes the caching sub-system that comes with the FreeType
library since release 2.0.6. As we'll see it provides a robust,
high-performance and flexible way to manage faces, sizes, glyph images
and charmaps.
The first section presents an overview of the cache manager and its main
APIs. The second one explains how it's possible to extend it to support
caching of custom data with very little efforts, due to its
extensible design.
I. Requirements and Design Goals:
When dealing with fonts, caching rapidly becomes a necessity for at least
the following reasons:
opening font files is relatively slow, since it needs to load data into
memory, perform a minimum amount of validation on the input, as well as
setup certain format-dependent tables which are much too esoteric to
be detailed here. As a consequence, it is always a good idea to keep
a FT_Face object opened as long as possible.
on the other hand, each FT_Face or FT_Size object
can take from a few hundreds to several dozen kilobytes each, depending
on the font and its format. Since it's not easy to determine this footprint,
it's a good idea to assume that it is always large, and limit the
number of opened faces to the minimum.
similarly, each face contains hundreds, if not thousands of distinct glyph
images. Since typical text only uses a fraction of a face's glyphs, loading
all glyphs at once is nearly never a good idea. Instead, it's better to
only retrieve and store the ones we need more often.
finally, when all glyphs belonging to a given face are already loaded
in memory, the corresponding FT_Face object can be closed to
reduce the total memory footprint.
The FreeType caching sub-system is nothing more than a powerful way to
automate all these tasks very easily.
II. The Cache Manager:
At the heart of the caching sub-system is a single object
called the cache manager, used to handle all kinds of data
within the cache. You should always begin your application by calling
the API
FTC_Manager_New
to create a new manager object.
1. Identifying faces through FTC_FaceIDs:
For reasons of portability and flexibility, the cache manager has no
direct knowledge of which fonts are "installed" or available during program
execution. Instead, the application is responsible of the tasks of
locating and identifying faces and opening the corresponding
font files.
What this means is that a client application must uniquely identify
any given available/installed face with a pointer, with type
FTC_FaceID
. The meaning of such pointers is not important to the cache manager,
but their values must be constant throughout program lifetime, and are used
to associate cached data to their corresponding faces.
The application must also provide (during FTC_Manager_New) a
special callback function, named a
FTC_Face_Requester
. The face requester is simply in charge of "translating" a given
FTC_FaceID into a fresh new FT_Face. This generally
implies calling FT_New_Face or FT_Open_Face, but
conveniently encapsulate any kind of font installation/listing problem
to the application side.
In most cases, a FTC_FaceID is a pointer to a structure that
contains a file pathname and a face index, and where the face requester
simply calls FT_New_Face, as illustrated by the following code:
/* define custom face identification structure */
typedef struct MyFaceRec_
{
const char* file_path;
int face_index;
} MyFaceRec, *MyFace;
/* our custom face requester is dead simple */
static FT_Error
my_face_requester( FTC_FaceID face_id,
FT_Library library,
FT_Pointer request_data,
FT_Face *aface )
{
MyFace face = (MyFace) face_id; // simple typecase
return FT_New_Face( library, face->file_path, face->face_index, aface );
}
{
FTC_Manager manager;
...
/* initialize cache manager */
error = FTC_Manager_New(
library,
0, /* use default */
0, /* use default */
0, /* use default */
& my_face_requester, /* use our requester */
NULL, /* don't need this. */
&manager );
}
What this code doesn't show is how the list of available/existing
FTC_FaceID/MyFace handle is generated, since this
is highly program or platform specific. For a more concrete example,
have a look at the file ftcommon.i in the "ft2demos"
package distributed by FreeType.
2. Caching FT_Face and FT_Size objects:
Client applications should not open new FT_Face objects directly
with
FT_New_Face
or
FT_Open_Face
. Instead, they should directly query them from the cache manager
with a call to the
FTC_Lookup_Face
API.
The reason for this is that the manager automatically limits the number
of opened FT_Face objects during program execution, according
to a pre-determined threshold. It does so by keeping all opened face
objects in a list sorted in most-recently-used (MRU) order.
When a new face object is requested and when the list if 'full', the
oldest FT_Face is automatically closed to leave room
for the new one. Each time you call FTC_Lookup_Face, the
returned face is also placed at the head of the manager's list.
Note that FTC_Lookup_Face takes a FTC_FaceID on input
and returns a valid FT_Face handle (or an error).
Similarly to FT_Face handles, the cache manager also handles
a list containing opened FT_Size objects, which correspond
to specific character pixel sizes of given FTC_FaceID handles.
As a consequence, applications should never try to set a face's current
character size with
FT_Set_Char_Size
or
FT_Set_Pixel_Sizes
on a given FT_Face. Instead, they should use
FTC_Lookup_Size
.
It's very important to note that FT_Face and
FT_Sizehandles returned by the cache manager should be
considered temporary objects, as these can be flushed out
of the cache any time you call any of the caching sub-system
APIs, even one that do not directly deal with these types.
It is indeed not possible to "lock" one of these objects within
the cache to prevent them from being flushed. This is not a bug but an
important design feature.
However, we'll see in the next section that it is possible to extract
a variety of data from these items (like glyph images, charmaps, etc..),
and cache them in a more friendly and efficient way..
II. The Cache Pool
The final purpose of the cache manager is to provide an "area", called
the cache pool, where specific font data can be cached in a
very efficient way. More specifically, this data is organized in
abstract cache "nodes".
1. Cache Nodes
Each cache node corresponds to a small fragment of the information
contained in a face that is likely to be used very often for text
rendering and other common operations. For examples: glyph images, bitmaps
or charmaps (though other types of data can be easily added as described
later). Cache nodes have the following properties:
the cache manager is capable of computing the exact memory
footprint of each cache node quickly, and keeps all nodes in a MRU list
as well.
when the pool is "full", the manager is capable of destroying the
"oldest" node(s) in order to create a new one.
each node is reference-counted, with an initial count of 0. The manager
will never destroy a node whose count isn't zero, and thus
supports "locking" nodes in the cache.
This allows the manager to limit the total memory size taken by cache
nodes (instead of their number), thus ensuring a controlled memory
footprint for all cached data. This means that, independently of the
number of faces and sizes used, these nodes will never take more than,
say, 300 KB, in memory.
It's of course possible to use different types of cache nodes within
the manager's pool. Notice that the "pool size" is set when calling
FTC_Manager_New and doesn't change afterwards. Its exact
value doesn't influence the API or the code of the programs using
it (only their performance, eventually).
All of this is performed automatically, but the cache manager doesn't
provide APIs to actually create new nodes in its pool. That's
the task of cache objects.
2. Cache Objects
A cache object is used to managed a single type of
nodes. It thus provides:
a high-level API to client applications, to let them lookup nodes.
(this function create new nodes when they're not available yet in
the cache pool)
a simple description of its cache nodes to the manager,
(which is hidden to client apps). Basically, this means how to
compute a node's footprint, how to destroy a node, etc..
Several cache objects can be used concurrently in a single manager.
Up to 16 caches can be used in a single cache manager, but this number
can be changed at build time through the definition of the FTC_MAX_CACHES
macro. For example, the ftview demo program uses three cache
objects provided by the default FT2 cache sub-system:
The high-level API of each cache is generally made of two important
functions:
Note that even though each Lookup function returns raw
data (like glyph images, metrics, etc..), it may also optionally return an
opaque FTC_Node handle corresponding to the cache node
where this data was found.
More precisely, the last parameter of some Lookup
routines is a FTC_Node* pointer, i.e. the address of a variable
that will receive the cache node handle. if this address is NULL, the
raw data is simply returned, and the node is left unchanged (and hidden
to the application).
If this address is _not_ NULL, the cache node handle is written to it,
and its reference-count is incremented before
Lookup returns. It's then up to the caller to later call
FTC_Node_Unref
to decrement it back.
Finally, you can also use
FTC_Node_Ref
to manually increment the reference count of a given cache node. You
should however never forget to call FTC_Node_Unref when you
don't need it anymore, in order to ensure correct behaviour of the
cache manager.
Leaving cache nodes "locked" in the cache leads to its saturation as
well as further lookup failures.
III. Designing custom cache types
If you're perfectly happy and satisfied by the three cache objects
provided with the standard cache manager (i.e. glyph images, small bitmaps
and charmaps), I advise you to jump directly to this document's conclusion.
Indeed, this section details the sophisticated subject of providing
custom cache object types in order to extend the caching sub-system.
We'll see that this is not very difficult and doesn't need large
amounts of code. However, its requires a good understanding of the
sub-system's internals that will be explained here..
1. Internals Overview
First of all, you should always include the file
FT_CACHE_MANAGER_H (a.k.a. <freetype/cache/ftcmanag.h>
in most installations) before your custom cache code, since it contains a bunch
of necessary type and functions declarations.
More specifically, it provides the definition of four important types
which will be treated as abstract object classes in the rest of this
section:
a. the FTC_Node class:
Each cache node is modeled by a FTC_Node handle, which is
a pointer to the
FTC_NodeRec
structure.
typedef struct FTC_NodeRec_
{
FTC_Node mru_next; /* circular mru list pointer */
FTC_Node mru_prev; /* circular mru list pointer */
FTC_Node link; /* used for hashing.. */
FT_UInt32 hash; /* used for hashing too.. */
FT_UShort fam_index; /* index of family the node belongs to */
FT_Short ref_count; /* reference count for this node.. */
} FTC_NodeRec;
It can be detailed with the following:
Each node is part of a MRU-ordered global list owned by the
cache manager, and thus contains two fields, named
mru_prev and mru_next, for that
purpose.
Each cache object implements a fast and dynamic hash table. As a
consequence, each node also contains a 32-bit field named
hash and a bucket list pointer named
link
Finally, each node contains a 16-bit reference count
(ref_count), and a 16-bit index
(fam_index) into a global list of "families"
(which are described below)
On a 32-bits system, the minimal size of a given cache node is thus
of 20 bytes.
You can "derive" the base FTC_Node class by defining your
own structures with a FTC_NodeRec as its first field, as
in:
typedef struct DummyNodeRec_
{
FTC_NodeRec node;
........ my node information
} DummyNodeRec, *DummyNode;
b. the FTC_Family class:
All the nodes managed by a given cache can be grouped in
families. Each family corresponds to a group of nodes sharing
the same runtime properties. It is modeled by a
FTC_Family
handle.
For example, both the FTC_ImageCache and
FTC_SBitsCache use families to group all image/bitmap nodes
belonging to the same face, size and format. More precisely, each
family object they manage contain a FTC_FaceID, character
pixel sizes and a glyph image format descriptor.
Each family belongs to a single cache object, and it has a globally
unique 16-bit index used to address it from a given cache node. If several
cache nodes belong to the same family, this allows efficient sharing of
information and prevent duplication of considerable amount of identical
data.
Just like FTC_Node, FTC_Family is an abstract class
that must be derived to create "real" types.
c. the FTC_Cache class:
As said previously, each FTC_Cache object implements an
optimized and dynamic hash table to hold all of its nodes. Additionally,
the cache controls a MRU list of families for its nodes.
When a lookup is performed in a cache, the latter begins by finding the
family corresponding to the request (if none is available, it creates a
new one). Once found, the family determines the hash value to use to
lookup the node in the hash table.
d. the FTC_Query class:
Which leads us to the FTC_Query class, which is used exclusively
within lookups to during lookups
When a cache is searched, the information relevant to select or create
the right node is gathered in a type called a "query". Its base class is
FTC_Query
, which must be derived too for "real" queries / lookups.
|