DESTINY 2

The UI format

The whole game UI ships as package data in three classes. A screen names views, a view names hierarchies, and a hierarchy is a node tree that names one widget table. The widget table holds the values and the bindings that drive each node.

Class ids are package classes. Native class names such as c_text_widget are the game’s own. Field names are descriptive.

classroleentries
0x808047B7screen20 entries, 10 payloads
0x8080496Ahierarchy340
0x80804825widget table1,254

Every screen is stored twice, as package file type 8 and type 16, with byte-identical payloads. Why is not known.

screen
  -> view (named)
     -> view entry -> hierarchy
        -> node tree
        -> widget table
           -> node bindings -> back to hierarchy nodes
           -> objects -> components -> property refs -> slot + pooled value

The array container

A UI blob starts with its own length. Everything after it is arrays reached through 16-byte descriptors. Every stored offset is relative to the field that holds it.

struct UiArrayRef {        /* 16 bytes */
    uint64_t count;        /* +0x00 */
    uint64_t relative;     /* +0x08 */
};  /* data header at: descriptor offset + 0x18 + relative */

struct UiArrayHeader {     /* 20 bytes, packed */
    uint32_t marker;       /* +0x00  always 0x80809FBD */
    uint64_t count;        /* +0x04  same as the descriptor's count */
    uint64_t item_class;   /* +0x0C  class of the elements */
};  /* elements start at +0x14 */

The element class is only in the header. A parser must read it there and dispatch on it. The 1,254 widget tables hold 54,510 arrays, and every one resolves this way.

Screens and hierarchies

struct UiScreen {                  /* class 0x808047B7, 80 bytes */
    uint64_t   length;             /* +0x00 */
    uint32_t   font_set;           /* +0x08  class 0x808047C5 */
    uint32_t   unused;             /* +0x0C */
    UiArrayRef views;              /* +0x10  class 0x808047CB */
    UiArrayRef resources;          /* +0x20  class 0x808047C6, {u32 tag, u32 flag} */
    uint8_t    filled[0x14];       /* +0x30  0xFF */
    uint32_t   optional_fonts;     /* +0x44  class 0x808047C7, or -1 */
    uint32_t   pad[2];             /* +0x48 */
};

struct UiView {                    /* class 0x808047CB, 24 bytes */
    uint32_t   name_hash;          /* +0x00  251 distinct over 570 views */
    uint32_t   pad;
    UiArrayRef entries;            /* +0x08  class 0x808047CD, {u32 name, u32 hierarchy} */
};

struct UiHierarchy {               /* class 0x8080496A, 40 bytes */
    uint64_t   length;             /* +0x00 */
    UiArrayRef nodes;              /* +0x08  class 0x80804616 */
    uint16_t   count_a, count_b;   /* +0x18 */
    uint32_t   widget_table;       /* +0x1C */
    uint32_t   pad[2];             /* +0x20 */
};

struct UiNode {                    /* 4 bytes */
    uint16_t parent;               /* +0x00  0x7FFF on the root */
    uint16_t id;                   /* +0x02 */
};

Node ids are a permutation of 0 to count - 1, local to the hierarchy. The array is not stored in id order, so read the tree through the id, not the array position. The client sizes its node array from the hierarchy’s count and refuses more than 0x1400 nodes.

Widget tables

The widget table root is 344 bytes: a length, eight array descriptors, then thirteen value-pool descriptors. The main arrays:

offsetclasselement
+80x80804607links to other widget tables, 12 bytes
+240x80804618the objects index, 4-byte ids
+560x80804622the components index, 16-byte rows
+720x8080462Aobjects, 64 bytes
+880x808046D8node bindings, 56 bytes
+1200x80804943slots, 16 bytes
+136 to +328severalvalue pools

The member names objects and components are Bungie’s. They name the index tables at +24 and +56, not the 64-byte object records. Widget tables link to each other, so the set is a graph, not a tree.

struct UiObject {                  /* class 0x8080462A, 64 bytes */
    uint8_t    type_hash[20];      /* +0x00  a type id: three values over the corpus */
    uint32_t   pad;                /* +0x14 */
    uint32_t   constant;           /* +0x18  0xB93A719D on every object */
    uint32_t   pad2;               /* +0x1C */
    UiArrayRef components;         /* +0x20  class 0x808046D4 */
    UiArrayRef animations;         /* +0x30  class 0x808046F3 */
};

struct UiComponent {               /* 24 bytes */
    uint64_t   key;                /* +0x00  low 16 bits: a node id, local to the resource */
    UiArrayRef property_refs;      /* +0x08  class 0x80804858 */
};

struct UiPropertyRef {             /* 24 bytes */
    int64_t  slot_rel;             /* +0x00  self-relative, to a slot */
    uint64_t selector;             /* +0x08  which property */
    int64_t  value_rel;            /* +0x10  self-relative, to a pooled value */
};

A component’s key is the node it sets properties on. The client adds the current import base to the key’s low 16 bits, checks the import span, and selects that node. A property reference binds a slot to one pooled value, and the selector says which property.

The object’s +0x30 array holds animation rows. Each row’s array holds animation targets, 32 bytes, with the target node id at +0.

Node bindings

A node binding reads one node’s property and writes another’s. The binding record and its transforms are on Behaviors.

Slots

A slot is itself an array descriptor, pointing at 1 to 5 elements of 4 bytes. An element is a pair of 16-bit halves. Element 0 draws from the selector space. Later elements are small integers. That a slot is a short typed tuple, a code then operands, is unverified.

Properties

Property ids are per type. A selector means nothing without the type that owns it.

The base layout type owns the 0x200 band. Every type that inherits it reads these the same way:

selectorproperty
0x201visible, a bool
0x206, 0x207X and Y, relative to the parent
0x208, 0x209width and height
0x20Fthe layout mode: 0 none, 1 the ordinary box constraint, 2 another constraint
0x210the box constraint: anchors, margins, minimum and maximum size

A type outside that line can reuse a number for something else. Native containers can overwrite the stored rectangle during layout, so the stored values are not the composed screen position.

Layout anchors

The box constraint at 0x210 holds a vertical anchor, a horizontal anchor, four margins and minimum and maximum sizes. Anchor modes:

modeplacement
0keep the stored position
1start padding plus start margin
2centered in the parent
3from the end: parent size minus child size, end padding and end margin
4stretch between the padded margins, within the size limits

The ordinary container’s 0x600 and 0x601 alias width and height. 0x602 and 0x603 turn on automatic height and width. 0x604 to 0x607 set its left, top, right and bottom padding. One container type distributes its children along an axis instead and must not be treated as the ordinary container.

Value pools and units

The value pools hold floats, vec4s, integers, 16-bit values and bytes. The two float pools have different units. One supplies a float directly. The other multiplies by the current screen height and rounds to the nearest even integer, unless the property’s metadata says otherwise.

Most stored coordinates are in units of screen height: 28,409 of 30,813 non-zero floats are exact multiples of 1/1080. So 1.0 is 1080 pixels and 1.777778 is the 16:9 aspect ratio. Not every value follows that rule.

The base reset makes a new node screen_width / 1920 * 360 wide and screen_height / 1080 * 240 tall. These are constructor values, not measured content sizes.

Text and bitmaps

Text

c_text_widget owns the 0x400 band.

selectorproperty
0x401minimum text width; not a font size
0x403, 0x404minimum and maximum height for automatic height
0x405the localized string hash
0x40Acase: original, uppercase, lowercase
0x40Cthe style key, looked up in a language-specific style table
0x40Dpalette key
0x40Ehorizontal alignment: left, center, right
0x414RGBA tint
0x41A, 0x41Bautomatic width and automatic height
0x420an explicit font size; zero uses the style’s size
0x421extra line spacing

The text color is the palette color times the tint times any animation tint. Storage is RGBA, with no swizzle. Technique 175, cui_font_complex, draws the glyphs. Its pixel shader takes alpha from the glyph mask times the color’s W.

When 0x40A has no override, the style row’s byte +17 supplies the case mode. Case conversion outside ASCII uses the game’s own glyph mappings, not standard Unicode casing.

Fonts

Font files are embedded in packages as OpenType (OTTO) data, package type 24. Type 24 alone does not mean a font; other data uses it too. A font manifest lists headers. A header holds the data tag, a filename and a byte length. The loader allows at most 16 MiB per file.

The screen’s font set holds thirteen language blocks. The client picks the current language and falls back to the first block when its style is missing. A style row (128 bytes) picks a font set of up to eight fonts, in authored order, and a default size. For example, the English waiting text uses NHaasGroteskTXPro-55Rg.otf at 22 pixels. The palette rows sit in the font set too, in four ranges; the normal range is first.

Line layout

The Latin line builder advances one UTF-16 code unit at a time. It does no OpenType ligature or GPOS substitution. It applies legacy pair kerning. Below 25 pixels per em the kerning is also scaled by the pixel size over 25. Kerning rounds to whole pixels, and glyph advances round to whole pixels with halves away from zero.

The builder breaks after a glyph with no ink, except U+00A0, U+202F and U+2060. It also breaks at hyphens and soft hyphens, and a newline ends the line. On overflow it moves the glyphs after the last break to the next line. With no break point, it splits before the glyph that overflows. Trailing empty glyphs are dropped.

The first baseline is one em below the origin. Each next baseline adds the tallest font-size height in the line before and the line spacing. Automatic height measures the text within 0x403 and 0x404. CJK text takes a separate path.

Bitmaps

c_bitmap_widget’s 0x600 sets its bitmap asset. 0x601 and 0x602 pick a group and a frame. Animation offsets add to both. A bitmap asset (class 0x80804A69) holds groups of frames, and each frame is a texture tag. A second layout with its own classes, marked by root byte +8 set to 1, holds the same shape. c_icon_widget picks its group with 0x606 and its frame with 0x607. A group index past the bitmap’s group count draws nothing.

A bitmap reference has two legal states: 0xFFFFFFFF for none, or a live handle. Any other value passes the “is set” test and resolves to an unallocated descriptor, which hangs the client.

Building and running a screen

Construction

The client builds a hierarchy’s widgets in order: imports first, then the resource’s own name and type arrays together. Each widget gets its name and a sequential index. Parent linking then skips the first hierarchy row and reads {parent, id} from every other row. A parent that is negative as a signed 16-bit value, or not below the allocated count, destroys that node. 0x7FFF is always out of range. Children are linked in stored row order, not id order.

Stored properties and binding endpoints are checked against the node slots before use. A binding needs both endpoints to resolve.

Screens at run time

A single screen manager holds two layers. Each layer is a linked list of open screens. The draw walks layer 0, then layer 1, each from head to tail. What the two layers mean is unverified.

A screen is inserted before the first screen with a higher priority. A stable sort then moves screens with flag bit 0x10 after the others. Each layer keeps a top active screen: the last screen in the list whose flag bit 0x2 is clear and whose widget state bit 0x4 is set. Whether that is the input focus is unverified. A screen with widget state bit 0x8000 is destroyed on the next layer update.

Every widget starts with the same head:

offsetfield
+16type descriptor
+24hierarchy tag
+2816-bit node index
+36name hash
+48parent
+56sibling link
+72child list head

So a widget’s hierarchy tag and node index match the package’s hierarchy and node id. A widget’s float slots 1 to 4 are its parent-local X, Y, width and height. Slot 6 is its visibility, set by 0x201. Slots 8 and 9 are a bitmap’s group and frame. An animation can add an offset to any slot.

A text widget picks its text from one of several sources. One is UTF-16 text, up to 1024 units. Another is UTF-8 text, decoded into 1024 units, with bad sequences shown as U+25A1.

Examples from the HUD

Waiting for Other Players

The label Waiting for Other Players sits in a HUD resource shared by the PvE, PvP and social HUDs. Its frame’s visibility comes from one inverted binding: the frame shows when a source node’s “ready” property is false. That property is local_ready && gameplay_state != 6. So the label means the client’s own HUD-ready test is false. It is not a count of other players.

local_ready needs two local state transitions to finish, and the activity lifetime state not to be 6, 7, 8 or 10. gameplay_state comes from one gameplay switch, key 0xB3C1251B. Payload 0xDC9B0994 selects state 6 and forces the label on. The label clears when the host replays the client’s exact activity-selection descriptor in activity message 1.

The waypoint presentation

The in-world waypoint presentation is c_cui_thud_waypoint_container. It receives a finished marker record, with screen position, heading and distance, and does not compute a route. Activities and destinations says where the marker comes from.

Open questions

  • The meaning of most selectors and of the slot element codes.
  • The three object type ids.
  • The UI name hash routine, which would name the 251 view hashes.
  • Why every screen is stored twice.
  • Which container rules produce the final rectangle.
  • What selector 0x202 does, and which widget state bit keeps a widget out of the draw.
  • What the two screen layers mean, and how a nested hierarchy joins a screen’s tree.
  • Behaviors – node bindings and the investment expression component.
  • Rendering – the renderer the UI draws through.
  • Packages – how the UI blobs are stored.
  • Edit CUI screens – editing these screens in Sunrise Studio.