This page contains technical information about FURWARE_text and is intended for developers and interested users.
|Tip: If you would like to get more info on specific topics, please leave a note on the discussion page. :)|
- 1 Terminology and hierarchy of entities
- 2 The main data structure for box, set and prim data
- 3 Basic flow of events
- 4 The initialization routine
- 5 The link_message(...)-handler
- 6 The setDirty(...)-function
- 7 The refresh()-function
- 8 Caching strategies
Terminology and hierarchy of entities
A "set" is a part of a text display consisting of display prims arranged in rows and columns. Each set has a name, usually assigned using the display creator tool. A linkset may contain multiple sets.
A "box" is a virtual text box within a set. Each set always has a (root) box of the size of the set. Each box has a name which is assigned via a parameter to the fw_addbox command. Additional boxes may be added to a set which are then stacked in layers, depending on the order in which the boxes were added.
This yields the following relations:
- A box is always part of (and fully contained within) a set.
- A set consists of one or more prims.
- A prim may have multiple faces.
- Each face shows one character of text.
The textual content of a box is often referred to as the box's "data" (note that "data" is sometimes also used with a more general meaning).
The text style configuration of a box (or parts of text) is often referred to as (the box's) "conf".
The main data structure for box, set and prim data
- "boxNameList" is a list containing the names of the boxes one after another. The reason for having the box names in a separate list (i.e. not part of "boxDataList", see below) is that the names shall be quickly searchable. For instance, when new text is received, the index of a box given its name must be retrieved.
- "boxDataList" is a strided list containing more information about each box. One stride contains:
- The box's text content (string).
- The box's style configuration (string).
- A bit-packed integer containing the box's layer (4 bits), the index of the set the box is part of (8 bits), dirty flags (2 bits), two reserved bits and the layer override bits (16 bits).
- The box's geometry (column offset, row offset, width, height) (rotation).
- "setDataList" is a strided list containing information about each set. One stride contains:
- "primLinkList" contains the link numbers of the prims, ordered by sets, rows and columns. The link numbers are not bit-packed because they shall be quickly searchable, e.g. when performing touch queries.
- "primFillList" contains bit-packed integers storing the state of each face on the respective prim. "The state" of a face means whether it is currently known to be showing whitespace or non-whitespace. This information is used as a cache to avoid SetPrimitiveParams-calls when not necessary.
- "primLayerList" contains bit-packed integers that store the assignment of faces to layers. The 32 bits of the integer are split into 4 bits for each face and thus each face may be assigned one of 16 different layer IDs. This information is used during rendering to quickly determine which layer a given face of a given prim belongs to.
Basic flow of events
This images is an attempt to visualize the basic flow of events and functions inside FURWARE text when a command that modifies the layout or contents of the display is received.
The initialization routine
This functions stands between the parsing of commands in link_message(...) and the actual rendering in refresh(...). The parameters are as follows:
One of the integer constants ACTION_CONTENT, ACTION_ADD_BOX or ACTION_DEL_BOX. This tells us what basic type of action shall be performed (for instance, setting new text is of type ACTION_CONTENT). This type is stored in a global variable and is reset in a call to refresh(...).
The reason for storing this information is the delayed refresh mechanism (which is in turn one of the reasons why setDirty(...) was introduced as a buffer between parsing and refreshing in the first place). At the end of setDirty(...), a timer is started with a quite short interval. The timer()-event merely calls refresh() once it fires. However, until then, more link_message(...)-events may be received and parsed which basically allows us to aggregate multiple commands (for instance, setting text for different boxes) before starting an actual refresh. This can yield a huge performance gain in some situations. Note that the timer is not restarted if the last action is already set to something else than 0 which "guarantees" that the timer will fire at some point, even if a constant stream of commands is received.
However, there's a catch. If we would just go and postpone all kinds of actions like setting text, styles, variables, adding and deleting boxes, we would end up with a mess and destroyed data structures (think about the details if you like, but it will make your head hurt). This is where the distinction between the different types of operations comes into play.
Operations that merely modify the contents of boxes are in some sense compatible to each other and allow us to aggregate them because the indices of the box data lists and layers remain constant during any content modifications and refreshes. Operations that add boxes may also be aggregated, broadly speaking because data will only be appended to some data lists and we can then refresh a batch of newly added boxes at once. Operations that delete boxes may be aggregated and we basically remember which layers have been freed during these operations. After that, we can just sweep through the remaining boxes and refresh the display accordingly.
Because mixing these types operations is a bad thing, each time a command is received, the handler checks whether the new operation is compatible with the last type of operation (that is, "last action type == new action type") and if it is not, an immediate refresh is forced before handling the command. Note that only the type of operation needs to be the same, not the exact command; for instance, fw_data and a fw_conf commands are compatible because they are both of type ACTION_CONTENT.
Note that the kind of action may also change the operation of the refresh()-function; please see the according section for details.
integer first / last
The interval of box indices to work on. This way we can operate on a whole range of boxes at once.
May be either 0 or 1. Used for determining whether to store given data (if any) as the data or conf of the box(es). Also, if isConf is true, drawing of the border of the box(es) is forced on next refresh because the border style may have changed.
This is where it gets a little tricky. :) Before we talk about how this value is used, a few facts: This parameter shall only be non-zero when adding or deleting boxes. It is stored bit-packed as part of the status of the box(es) in the boxDataList and is cleared after a refresh() is completed. The value itself is 16 bits long and each bit stands for one of the 16 layers available within a set. A value of 0xFFFF (binary 1111111111111111) thus means "all layers" and a value of 0x4 (binary 0000000000000100) means "layer 2" (counted from 0).
The value is used in the draw(...)-function which is called at several places in refresh() to draw a character to a face. Normally, draw(...) fetches the index of the layer that is currently in the foreground at the specified face as well as the layer of the box whose contents shall be drawn. If these two values are not equal, it will reject to update that face because the box to be drawn is not in the foreground.
Now if we added or removed a box, the stored information about which layers are in the foreground may change for some faces. This is just what the value of the newLayerOverrideBits tells us. For instance, when a box was added, the bits are set to 0xFFFF. Now when the newly added box is being drawn, the draw(...)-function will see that it is supposed to override any other layer and draw the new box on top. When deleting a box, the logic is similar, however only specific bits will be set and the order of the refresh routine is reversed; also see the section about the refresh()-function.
Allows us to restrict writing data to boxes that are part of a certain set. A value of -1 means to operate on boxes belonging to any set.
Tells us whether the data-parameter (see below) contains data to be stored. This allows us to differentiate between the empty string and "no data given".
Contains either new text content or a new style config string that is to be stored in the boxDataList.
The refresh()-function may be seen as the "heart" of the text script. It is either called after some changes have been aggregated or when a refresh needs to be forced during command handling.
It basically consists of a loop that iterates through all boxes and checks their "dirty" bit. If a box is marked dirty, its current data (text, style, geometry) is loaded and prepared for drawing.
A word on the order of iteration of this outer loop: Normally, the boxes are iterated in the reverse order in which they have been added. That means that boxes will be refreshed "top-to-bottom" (with respect to the layers). However, there is an exception: When the type of the last command(s) was ACTION_ADD_BOX (see the "action" parameter of setDirty(...)), the boxes are iterated in the order in which they were added to the board (bottom-to-top). This is necessary to correctly update the information which faces currently show what layer (also see the "newLayerOverrideBits" parameter of setDirty(...)).
Back to the main loop and the dirty bit. There are actually two dirty bits: One telling us that the box needs to be refreshed at all and one that the border of the box also needs to be refreshed. For instance, if only the text content of the box was changed – but not its style settings – then there is no need to bother with re-drawing the border.
The text of the box is split into parts (at whitespaces, newlines, style markup tags) and an inner loop iterates through these tokens which prepares lists of character indices and style information given right within the text as markup. Once a row worth of data has been prepared (row = a row of primitives), another small loop renders the text to the faces before data preparation for the next row begins.
After a box has been refreshed, its dirty bits are reset. When all boxes are done, the last action is also reset and (if the notification option is enabled) a "fw_done" message is emitted to the linkset.