DESTINY 2

Rendering and effects

The renderer is data driven. Packages carry the input layouts, the shaders, small bytecode programs that fill shader constants, and the placed lights, atmospheres, water and effects. The executable supplies the passes that read them. The build is D3D11.

Stage, scope and technique names such as transparents or global_lighting are the game’s own. Function and field names are descriptive labels. Hex values like 0x80806E28 are package class ids or tags. For how tags and classes work, see Packages.

The facts come from the executable, the installed packages, and a standalone viewer that runs the game’s own shaders. Visual parity with the game is not verified.

The launch record

One package record, class 0x80806CB1, is the renderer’s root. It holds:

offsetcontent
+0x08the input declaration resource
+0x10the scope array, 16-byte rows
+0x20the technique array, 16-byte rows; 623 rows
+0x30a link to the global textures table

Native input declarations

Input layouts come from package data, not from shader reflection. A declaration resource holds format groups and declarations.

struct VertexElement {       /* 3 bytes, class 0x808072B2 */
    uint8_t semantic;        /* +0x00  index into the native semantic name table */
    uint8_t semantic_index;  /* +0x01 */
    uint8_t format;          /* +0x02  index into the native format table */
};

struct InputDeclaration {    /* 28 bytes, class 0x808072AC */
    uint16_t id;             /* +0x00 */
    uint8_t  instanced[4];   /* +0x02  per-stream instance flags */
    uint8_t  pad[2];
    int32_t  group[4];       /* +0x08  format group per stream; -1 omits it; width assumed */
    /* rest not read */
};

The engine turns each declaration into D3D11_INPUT_ELEMENT_DESC records. Each format id selects a native row with a DXGI format, a byte width and a fallback format id. A zero DXGI format falls back.

A static mesh picks its declaration per part and per stage:

struct StaticAssignment {    /* 8 bytes, class 0x8080719B */
    uint16_t part;           /* +0x00 */
    uint16_t stage;          /* +0x02  render stage */
    int16_t  declaration;    /* +0x04 */
    uint16_t pad;
};  /* row i uses material i of the mesh header */

One part can draw in several stages, each with its own material. Parts with detail category 7 draw only in shadow_generate and sometimes depth_prepass. They are shadow casters, not a detail level.

Render stages

The draw loop walks stages 0 to 22. A few by name:

stagename
0generate_gbuffer
3shadow_generate
6additive decals
7transparents
9light_shaft_occlusion
12depth_prepass
13water_reflection
20cubemap volumes
22world_forces

A stage number does not give the pass order.

Render states

A draw’s state is four packed bytes: blend, depth/stencil, rasterizer and depth bias. The low seven bits select a native state index. A byte with its high bit set overrides the one below it:

uint8_t ResolveStateByte(uint8_t pass, uint8_t material, uint8_t forced)
{
    uint8_t b = pass;
    if (material & 0x80) b = material;   /* material override */
    if (forced   & 0x80) b = forced;     /* final forced override */
    return b & 0x7F;                     /* native state index */
}

The state catalog holds 90 blend states, 83 depth/stencil states in two variants, 9 rasterizer states and 9 bias states.

Scopes and TFX

A shader’s constants are not filled by code alone. Each material and each scope carries a small bytecode program, called TFX here. It computes constant vectors from engine inputs, then the engine uploads them.

Scopes

A scope is a shared constant block, such as per-frame or per-view data. The launch record lists them by index. A few:

indexname
0frame
1view
2rigid_model
7skinning
9chunk_model
11instances
13transparent
14transparent_advanced
16terrain

Each scope has one block per shader stage. The pixel block starts at +0x40 and the vertex block at +0xD8:

struct StageBlock {
    ArrayRef textures;         /* +0x00  texture bindings */
    uint8_t  pad0[8];
    ArrayRef tfx_bytecode;     /* +0x18 */
    ArrayRef tfx_constants;    /* +0x28  vec4 constants the program reads */
    ArrayRef samplers;         /* +0x38 */
    ArrayRef initial_vectors;  /* +0x48  constant buffer template */
    uint8_t  pad1[0x20];
    uint32_t cb_slot;          /* +0x78  constant buffer slot */
    uint32_t extern_cb_tag;    /* +0x7C */
};

ArrayRef is a 16-byte package array reference: a count and an offset. The other shader stages (hull, domain, geometry, compute) have their own blocks. Those are not read.

Scope 0 frame is special. The engine fills it with direct code and skips its program. Its packaged template is all zero and is not the runtime value.

Externs

A program reads engine data through externs. An extern is a block the engine fills before the draw, held in a per-context table. The known ones:

externcontent
1the frame: time, exposure, frame constants, a few global textures
2the view: matrices and viewport
3view targets: depth, G-buffer, lighting buffers, the sky hemisphere
7the atmosphere
8a rigid model: instance matrix and model constants
39transparent pass inputs
41, 42global lighting inputs
43the current object effect
44additive decal inputs
67water, per object

The TFX interpreter

The interpreter is a small stack machine over float4 values. The switch has 85 cases. The table lists the decoded ones. The opcode numbers differ from newer public lists.

byteoperation
0x01, 0x06add
0x02subtract
0x03, 0x05multiply, per lane
0x04divide, guarded near zero
0x0Astep: 1 where B is not below A
0x0C, 0x0Dbuild a vector from lanes of two values
0x0Fcubic (B.x*A + B.y)*A*A + (B.z*A + B.w)
0x10lerp
0x12multiply-add
0x13clamp between two values
0x17, 0x1Afloor, fraction
0x1F, 0x20cosine-like wave; sin/cos pair
0x23saturate
0x25, 0x26log2 approximation, vector length
0x28, 0x29, 0x2Anoise waves and a floor hash
0x2Evector times a 4x4 matrix
0x34, 0x35push a program constant; lerp between two
0x37, 0x38, 0x39piecewise cubic curves
0x3A, 0x3B4-ramp and 8-ramp gradients
0x3C, 0x3D, 0x3Epush an extern float, vector, or four vectors
0x43, 0x44pop to one output vector, or to four
0x49, 0x4Cbind a sampler; push an authored sampler
0x4Dpush an object channel value

The rest are not decoded. The engine keeps exact float order in the waves and curves, so a port must not fuse multiply and add.

/* Extern reads: two operand bytes, extern id then element index. */
case 0x3D:  /* push extern vector */
    ext = ctx->externs[op[1]];            /* one pointer per extern id */
    push(((float4 *)ext)[op[2]]);
    break;
case 0x3C:  /* push extern float, splatted */
    ext = ctx->externs[op[1]];
    push(splat(((float *)ext)[op[2]]));
    break;

TFX parameters

Materials also read named global parameters. There are 153, each keyed by a 32-bit hash with an authored default. Every frame the engine rebuilds a runtime table from those defaults, in a fixed order.

void Render_ResolveParameters(float4 *runtime, const Param *authored, int count)
{
    /* 1. keyed overrides from a variable bank */
    for (int i = 0; i < count; i++) {
        float4 v;
        if (VariableBank_ReadVec4ByKey(active_bank, authored[i].key, &v))
            runtime[i] = v;
        else if (restore_defaults_when_missing)
            runtime[i] = authored[i].value;
    }

    /* 2. a modifier list of 16-byte rows: {index, lane, value, op} */
    for (Modifier *m = modifiers; m; m = m->next) {
        float *x = &runtime[m->index].v[m->lane];
        switch (m->op) {
        case 0: *x = lerp(*x, m->value, weight); break;  /* weight source not read */
        case 1: *x += m->value; break;
        case 2: *x -= m->value; break;
        case 3: *x *= m->value; break;
        case 4: *x  = m->value; break;
        }
    }

    /* 3. placed parameter volumes, sorted by an int field, applied in order */
    for (Volume *v : sorted(placed_parameter_volumes))
        Volume_Apply(v, runtime);
}

The package classes behind the variable bank and the placed volumes are not verified.

Parameter key 0xE638AAEB scales every ambient color in global_lighting_ambient_only. Its authored default is zero. Without an override the ambient term is black.

View and frame data

View matrices

Extern 2 holds the view as row vectors, with no transpose on upload. V is world-to-view, P the projection, C the pixel-to-clip matrix.

matrix indexvalue
2V
6P
10inverse(V)
14inverse(P)
18V * P
22inverse(V * P)
26C * inverse(V * P)
30C * inverse(V * P) * V
34U * inverse(V * P), where U is C for a 1x1 target
50V without translation

C has rows (2/w, 0, 0, 0), (0, -2/h, 0, 0), (0, 0, 1, 0) and (-1, 1, 0, 1). The viewport vector is (w, h, 1/w, 1/h).

The frame clock

Extern 1 float 0 is game time in seconds, wrapped at 28,800 s (8 hours). The engine counts time in ticks of 1/673,200 s and wraps before it converts to float. Floats 1 to 4 hold the time again, the fraction of the current second, time modulo 420 s, and the time of day.

The time of day is a 0-to-1 value. Its reset value is 0.2, which shows night on the EDZ sky. The day is 3,600 s long by default. The running game advances it.

Float 7 is 2^EV, the exposure. The exposure value starts at 0 and is clamped to [-3, 3] by default.

Model constants

For a rigid model, extern 8 holds the instance matrix in vectors 0 to 3, three model header vectors in 4 to 6, and a draw vector in 7. The rigid_model scope copies them to vertex constant buffer 11.

Skinned and chunked models read a transform palette from the same buffer, from row 8:

formrows per entryentry
matrix3columns 0 to 2 of the world matrix, translation in W
dual quaternion2rotation, then dual part

Vertices are stored in object space. How the game sizes the palette window is not verified.

Terrain

Terrain draws parts as triangle strips with input declaration 60. Each 12-byte part row holds a material, a first index, a count, a group and a detail level. A part draws when its group is visible and its detail level equals the group’s selected level. Per-vertex ambient occlusion comes from a byte buffer indexed by vertex id.

Lighting pass and the ambient technique

The lighting pass runs after the G-buffer stage. Its order is fixed:

  1. clear_lighting_buffers: clear the three lighting targets to zero.
  2. cubemaps_render_pass: technique cubemap_apply_sky_copy_ao, which applies the sky hemisphere and ambient occlusion. A global flag selects a different technique instead.
  3. Placed cubemap volumes (stage 20), or three other techniques when a global flag is set.
  4. dominant_lighting_apply: draw the global lighting technique selected for the view.

The three lighting targets are lighting_diffuse, lighting_specular and lighting_ibl_specular, all R11G11B10_FLOAT.

The global lighting technique comes from a 16-row table. The read rows select global_lighting, global_lighting_ambient_only, or a third technique between them. The ambient technique draws a full-screen quad with additive blend and a depth test with no depth write. It reads the exposure from extern 1.

Sky hemisphere

Reflections and sky light read a generated hemisphere map, not a package cubemap. The engine renders the sky into it, tints it, and filters mip levels 1 to 9 with a GGX filter. The base size is 512, format R16G16B16A16_FLOAT.

Placed cubemap volumes

Local reflections are placed boxes, resource class 0x80806B7F. Each holds a world matrix, extents, fade vectors, an intensity, an optional grid and three textures. Their matrices already contain the world placement. The engine picks one of many techniques from a six-bit key built from which features the volume uses.

Deferred shading and fog

After lighting, the shading pass draws the sky first, marking sky pixels in the stencil. Then it draws deferred_shading over every non-sky pixel. That shader lights the G-buffer and applies atmospheric fog:

color = lit * transmittance + (inscatter + tint * haze) * fog_scale * exposure;

Atmosphere

The atmosphere is a placed resource, not a standalone tag. A map’s placement list carries it, together with a time-of-day resource.

struct AtmosphereResource {        /* class 0x80807086, fields from +0x10 */
    uint32_t keyframes_a[16];      /* +0x10  handles; empty in all shipped data */
    uint32_t keyframes_b[16];      /* +0x50  handles; empty in all shipped data */
    uint32_t lookup_3d;            /* +0x90  128x128x16, one slice per time key */
    uint32_t lookup_3d_second;     /* +0x94  optional, blended in */
    uint32_t lookup_2d;            /* +0x98  16x1024 */
    uint32_t unused_tag;           /* +0x9C  not read by the fill */
    float    time_keys[16];        /* +0xA0  rising, wrapping at 1 */
    float4   clear_color;          /* +0xE0  for mode 4 */
};

The 3D lookup is sampled by view azimuth, view height and time key. The engine finds the two keys around the time of day and blends them.

The atmosphere also has 30 keyed parameters: fog densities, height falloffs, phase terms. Two height fogs fall off with camera height as exp(-(z - base) * falloff / 1000).

Sky modes

modetechniqueoutput
0skythe generated sky lookup
1sky_ref_atmsame material as mode 0
2sky_no_atmblack
3sky_grognok_atmsame material as mode 0
4sky_grognok_cleara clear color

With no atmosphere loaded, the mode is 2 and the sky is black.

The light direction

The time-of-day resource holds a day length, a start time and a curve set. The curve set has two time windows and four curves: the sky direction and the light direction, each inside and outside its window. The engine samples them every frame at the time of day. The result is the sun direction, which also feeds the shadows.

The sky lookup

Each frame the engine builds a screen-space sky lookup in these passes:

  1. sky_generate_sky_mask: a 64x64 mask from min/max depth.
  2. downsample_block_2x2.
  3. Two radial blurs toward the light, for light shafts.
  4. sky_hemisphere_seed_inscattering and sky_hemisphere_spherical_blur.
  5. sky_lookup_generate, which blends the 3D lookups and adds a glow around the light.

Water

Water is a render feature. Maps place it as resource class 0x80806DE0: a model, bounds and a physics shape. It draws in two stages:

stagewhat it does
13 water_reflectionplanar reflection; skipped when the camera is more than 0.5 units behind the plane
7 transparentsthe surface

The surface draw first marks its pixels in the stencil. The water material then draws only where that mark is set. Extern 67 carries its per-object inputs: position, scale, a refraction copy of the lit scene, a planar reflection, ripple normals and a shadow term. The water shader also reads the atmosphere lookups for fog and the sky hemisphere for reflection.

Effects

Maps place effects in two ways. Both go through the map’s placement rows.

routecarried by
particle nodesinside the placed entity’s component configs
lens flaresa placement resource, class 0x80806CBF, that names a flare

Particle nodes

A particle node is an inline record inside an entity config. It names one particle system per row.

struct ParticleNode {              /* class 0x80806CC6, 296 bytes */
    uint32_t name_hash;            /* +0x00  FNV-1 of a name such as "sparks" */
    uint16_t pad;
    uint16_t parent;               /* +0x06  assumed */
    uint8_t  pad1[4];
    float    start[2];             /* +0x0C  assumed */
    float    duration[2];          /* +0x14  assumed */
    uint8_t  pad2[0x14];
    ArrayRef systems;              /* +0x30  class 0x80806CC8 rows, 24 bytes each */
};

A system row can hold 0xFFFFFFFF instead of a tag. The config then lists variants, and the runtime picks one. How it picks is not verified.

Particle systems

struct ParticleSystem {            /* class 0x80806E28, 52 bytes */
    uint32_t emitter;              /* +0x00  class 0x80806E2C */
    uint32_t compute[2];           /* +0x04  compute materials, when GPU-simulated */
    uint8_t  pad[4];
    uint32_t compute_third;        /* +0x10 */
    uint32_t raster_material;      /* +0x14 */
    uint32_t models;               /* +0x18  lists entity models */
    uint32_t material;             /* +0x1C */
    uint32_t hash;                 /* +0x20 */
    uint8_t  rest[0x10];           /* +0x24 */
};

About a quarter of systems simulate on the GPU with compute shaders. The rest do not. The simulation and its output stream layout are not decoded.

Emitters and the channel map

The emitter, class 0x80806E2C, is 336 bytes. It holds a constant array, a small program with its own constants, and a channel map.

struct EmitterChannel {            /* 2 bytes */
    uint8_t kind;                  /* 0xFF unused; 6 = a constant */
    uint8_t slot;                  /* for kind 6: float index into the constant array */
};

struct Emitter {
    uint8_t        pad[8];
    ArrayRef       constants;      /* +0x08  vec4 array */
    uint8_t        pad1[0x38];
    ArrayRef       program;        /* +0x50  bytecode */
    ArrayRef       program_consts; /* +0x60 */
    uint8_t        pad2[0x10];
    EmitterChannel channels[55];   /* +0x80 */
};

The 55 channels follow a fixed name list in the executable, from particle_age to emitter_grid_count. Examples: particle_lifetime, particle_velocity, emission_rate, emitter_radius, initial_speed, particle_max_count.

Kind 6 as “a constant from the array” is assumed. It fits a census of all 26,250 emitters: every slot is in range, and emission_rate and particle_max_count are whole numbers in 98 to 99 percent of them. Per-particle channels such as particle_position always use kind 1. Kinds 0 to 5 and the emitter program are not decoded.

Lens flares

A lens flare, class 0x80806F68, holds an array of 12-byte rows: a material, a parameter block and a flag. The flare shader builds a screen-space quad at a fixed clip depth of 0.99. The lens-flare role is assumed from that shader.

Object effects

Some materials read per-object values with TFX opcode 0x4D. The value comes from a channel table on the object. The table’s values come from value providers in the entity, which inputs and controllers update at run time. All shipped providers start at zero, so the at-rest value is set at run time.

Frame fibers

The client runs each frame as a job graph over three engine fibers: one for simulation and physics, and two render fibers with 8 and 7 phases. The network tick runs only after all three fibers finish the frame. So a render job that never finishes also stops networking.

The UI package format

The whole game UI ships as package data in three classes:

classrole
0x808047B7screen
0x8080496Ahierarchy
0x80804825widget table

The chain runs screen -> views -> hierarchies -> widget tables:

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.

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 */
    uint8_t    filled[0x14];       /* +0x30  0xFF */
    uint32_t   optional_fonts;     /* +0x44 */
    uint32_t   pad[2];             /* +0x48 */
};

struct UiView {                    /* class 0x808047CB, 24 bytes */
    uint32_t   name_hash;          /* +0x00 */
    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 local to the hierarchy. The array is not stored in id order, so read the tree through the id, not the array position.

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
+720x8080462Aobjects, 64 bytes
+880x808046D8node bindings, 56 bytes
+1200x80804943slots, 16 bytes
+136 to +328severalvalue pools: bytes, 16-bit, ints, floats, vec4s

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, not per object */
    uint32_t   pad[3];             /* +0x14 */
    UiArrayRef components;         /* +0x20  class 0x808046D4 */
    UiArrayRef extra;              /* +0x30  class 0x808046F3 */
};

struct UiComponent {               /* 24 bytes */
    uint64_t   key;                /* +0x00  small integer; meaning not known */
    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 */
};

struct UiNodeBinding {             /* class 0x808046D8, 56 bytes */
    uint16_t node_a, node_a_copy;  /* +0x00  hierarchy node id */
    uint32_t pad0;
    int64_t  slot_a_rel;           /* +0x08 */
    uint64_t selector_a;           /* +0x10 */
    uint16_t node_b, node_b_copy;  /* +0x18 */
    uint32_t pad1;
    int64_t  slot_b_rel;           /* +0x20 */
    uint64_t selector_b;           /* +0x28 */
    uint64_t pad2;                 /* +0x30 */
};

A node binding points from the widget table to a hierarchy node. A property reference binds a slot to a pooled value. Both meet at the same kind of slot. A slot is itself a short array of 4-byte codes. What the codes and selectors mean is not known.

Positions and sizes are in units of screen height. Most float values 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.

Open questions

  • Most TFX opcodes.
  • The hull, domain, geometry and compute stage blocks.
  • The package classes that drive runtime parameter overrides.
  • The particle simulation, emitter programs and channel kinds 0 to 5.
  • The producers of several extern values and pass states.
  • The meaning of UI selectors and slot codes.