Rendering
The engine's render layer on PS4: the backend boundary, resources, surfaces, command recording and submission, draws, state, and what the GPU hands back to the CPU.
The engine renders through a thin Bungie render API. Under it sits Sony’s own graphics library, linked into the executable. Nothing outside the render layer writes GPU commands. What reaches the render API is GPU-level data: raw texture, sampler and buffer descriptors, render targets as raw register sets, and shader structs that point at GCN machine code.
Two layers
layer
what it is
render API
Bungie code: contexts, draws, state, clears, queries, recording and submission, plus frame, surface and job code
libGnm, GpuAddress, libGnmx
Sony’s library, linked statically: descriptor helpers, the command packet writers, tiling math, and GfxContext with its constant update engine
- Code outside the render layer calls Sony’s library only to set up descriptors and to do tiling math: the surface allocator, lookup textures and the crash screen. The UI draws through the same render API as the world.
- One function submits work to the GPU. It is the only caller of both Sony submit calls.
The backend boundary
There is no virtual render interface in any Tiger build. Each platform links its own version of the same plain functions, with the same engine-side arguments. Rainbow Six Seige PS4’s RTTI names no device, context, resource or state class either.
The boundary has three parts, and each platform implements all three:
- The context API: per-context state, draws, clears, queries and recording. The first argument is always the engine context.
- The resource layer: a 15-slot callback table the resource system calls per package tag, plus buffer and vertex format creation called by the engine.
- The surface layer: render target allocation, binding as a target or a texture, clears and readback.
The engine reaches a context through a field at +0x15C0 of a per-job render state object. It is a plain data pointer, not a function table. Calls whose first argument comes from that field reach 154 functions on PS4 and 206 on Xbox, and they pair one to one.
The two draw calls differ slightly between platforms:
draw
PS4 arguments
Xbox arguments
indexed
context, topology, base vertex (ignored), start index, count
context, topology, base vertex, start index, count
non-indexed
context, topology, count
context, topology, start, count
Resources
The resource callback table
At start-up the backend registers 15 functions with the resource system. Xbox registers its 15 in the same order. Registering 0 installs the “no GPU” table. The resource system calls each slot with the tag’s type byte, its handle and its GPU data address.
slot
role
1
header size per type
2
on load: create
3
on unload: destroy
4
on move: fix addresses after a heap move
5
create a run-time texture; kinds 0, 1 and 2 give engine formats 72, 83 and 71
6
size of a run-time texture
7
fill a composite texture from source tags
8
no-op
9
GPU copy for a move, through the blitter ring
10
memory swap copy
11
point a texture at new mip memory, for streaming
12
are all mips resident
13
a flag; true on PS4
14
memory offset and size
15
heap choice from flag bit 1
On PS4 the unload slot is never called. Xbox calls it on two load events. This rests on a static cross-reference scan, which can miss calls, so it is unverified.
What loading does per type
The type ids are the same on both builds. The PS4 handler only fixes the addresses in a descriptor the package already holds. The Xbox handler creates a D3D object.
type
kind
PS4 on load
Xbox on load
1 to 3
texture
sets the texture descriptor’s base address
creates a placement texture over the data, and a view
4
vertex buffer
address at +0x28, count at +0x30
a buffer object at +0x28
6
index buffer
address at +0x30, 32-bit flag at +0x38, count at +0x3C
a buffer object at +0x30
7
constant buffer
base address into the buffer descriptor at +0
a dynamic constant buffer at +0x18
8
pixel shader
program address at +0x08 and +0x0C
a shader object at +0x08
9
vertex shader
program address at +0x1C and +0x20, fetch shader at +0x08
a shader object
10
geometry shader; two programs on PS4
fixes both
a shader object
11, 14
assumed: LS and ES stages, with a fetch shader
fixes addresses
no-op
12, 13
assumed: HS and CS stages
fixes addresses
no-op
16
sampler
no-op: the sampler descriptor is in the header
creates a sampler state
18
unknown
a handler exists
a handler exists
Xbox stores its D3D object where PS4 stores the GPU address. Each engine object has one native object, made in the load slot.
Textures
Neither build converts textures at load. PS4 packages hold a complete GCN texture descriptor and pre-tiled GCN data. Xbox packages hold an Xbox header, with a DXGI format and a Durango tile mode, and pre-tiled Durango data.
The PS4 texture header is 0x54 bytes:
offset
field
+0x00
the 32-byte GCN texture descriptor
+0x28, +0x2A, +0x2C
16-bit width, height and depth
+0x31
mip count
+0x34
has streamed mips
+0x35
block-compressed
+0x40
dimension
- Streaming points the descriptor at the streamed data.
- Defragmenting moves data through the blitter ring, then calls the move slot.
- The device makes two 256 x 256 RGBA8 default textures, white and black, and one default sampler.
- Built-in vertex and pixel shaders are parsed from embedded shader blocks. The vertex shader gets a fetch shader generated at run time. Package shaders ship with a prebuilt fetch shader.
When CPU data becomes final
PS4 never renames a buffer. Writes land in place. A renderer that runs work later must copy or version the data at the point it becomes final.
- Per-draw constants are allocated inside the command buffer. The allocation returns a CPU pointer, and the engine fills it after the call. They are final when the draw or the recording closes.
- Dynamic vertex buffers are final at unmap.
- UI vertices are copied into the command buffer at the draw call.
- CPU-written surfaces, such as video planes, are final at unlock.
- The glyph cache is final at its commit. It is multi-buffered.
The Xbox build does the same in its own way. It lays placement resources over engine memory, so the CPU writes them with memcpy as on PS4. Its per-draw constants come from three 8 MiB rings, bound by GPU address before the CPU fills them. Map and unmap are used only for dynamic vertex and index buffers, the back buffer and screenshots.
Surfaces
One surface manager holds every render target.
- 96 surfaces of 224 bytes, numbers 22 to 95 on the free list, and 256 slot surfaces.
- One heap of
0x1C210000bytes, plus a CPU-readable twin heap of0x74000bytes. - 20 fixed surfaces and 20 named recipes, set up once at device start-up.
- Allocators: bump, slot and arena.
A surface entry:
offset
field
+0x00
flags: the recipe in the low 16 bits; 0x8000 allocated; 0x100000 depth
+0x02
built bits
+0x04, +0x06, +0x08
16-bit width, height and slice count
+0x0C
kind
+0x18
a copy of the recipe
+0x40
GPU address
+0x48
CPU-readable twin address
+0x50
size
+0x54
Gnm depth render target
+0x88
Gnm render target
+0xB4
texture descriptor
Recipe flag 4 forces a linear layout. Flag 0x4000 adds fast-clear metadata (CMASK). Flag 8 gives the surface a CPU-readable twin. There are 13 surface formats; 7 and 8 are depth.
Recording and submission
item
fact
device manager
one block of 151,440 bytes; the device sits at +0x11B50
contexts
7, made once: one primary, bound to a thread through a TLS slot, and 6 deferred for parallel render jobs
Gnmx contexts
two per engine context, used in turn; the active one is at context +0x50
ring
64 record pointers
record
0x938 bytes: a ready flag, a label slot, up to 64 command buffer segments with their sizes, and a flip flag
Submission follows ring order, not the order jobs finish.
- A frame snapshots the primary context’s state, closes its current record and reserves one record per render job.
- Each job takes a deferred context, restores the snapshot, records, and finishes.
- Closing a record marks it ready. It enters the ring only if it holds commands.
- The consumer walks the ring in order and stops at the first record that is not ready. Before each submit it waits until the GPU’s frame label catches up, sleeping 300 us between checks.
- It submits the record, and flips when the record asks for it. On a flip error it rebuilds a minimal flip record and tries again.
A frame runs: begin frame, which waits for the flip and binds the back buffer; render; end frame; close the last record with a flip and consume; then frame end.
Frame end rotates the display buffer index modulo 3, hides the system splash screen once, and calls Sony’s “submit done” every frame. So even a frame that draws nothing still reaches the GPU library there.
The draw path
draw
arguments
notes
indexed
context, topology, unused, start index, count
skipped when the context’s draw-enable byte at +0x26 is 0
non-indexed
context, topology, vertex count
skipped the same way
indexed, instanced
context, topology, count, unused, instances, unused, start index
sets the instance count, draws, resets it to 1
non-indexed, instanced
context, topology, vertex count, unused, instances
the same
indexed from memory
context, topology, start index, primitive count
16-bit indices; no draw-enable check; the title screen UI uses it
from CPU vertices
context, topology, primitive count, vertices, stride
copies the vertices into the command buffer; draws nothing without an input layout; no draw-enable check
compute
context, compute shader, groups x, y, z, wait
one call site in the whole build, used by two helper passes
full-screen rect
context
the device’s own shaders, 3 vertices as a rect list
Topologies:
id
topology
vertices for n primitives
0
point list
n
1
line list
2n
2
line strip
n + 1
3
triangle list
3n
4
triangle fan
n + 2
5
triangle strip
n + 2
6
rect list
3n
7
quad list
4n
8 to 11
patch
12 to 15
adjacency
The index type is 0 for 16-bit and 1 for 32-bit. Rect list and quad list are the GCN primitive types of the same names.
The Xbox build maps topologies 4, 6 and 7 to “undefined”. So engine code shared with Xbox never uses fans, rect lists or quad lists. On PS4 they appear inside the backend and in the UI.
State
Context fields
offset
field
+0x26
draw enable
+0x50, +0x58
the active Gnmx context and the active ring record
+0x1D28, +0x1D30
the bound vertex and pixel shader
+0x1D60
input layout: element count, a stride per stream, then 8-byte elements of format, byte offset and stream
+0x1D68, +0x1D70
cached index handle and index record
+0x1D78, +0x1DF8
cached vertex handle and offset per stream, 32 streams
+0x1E78, +0x21F8
texture and sampler descriptor pointers, 7 stages x 16 slots
+0x2578
viewport: x, y, width, height, min z, max z
+0x2590
scissor
+0x25B4 to +0x25C0
blend, depth-stencil, depth bias and raster preset ids
+0x25D0
clip control register
+0x25D4
raster mode register
+0x25D8
blend control register, the same for all four colour targets
+0x25DC, +0x25E0
depth control and stencil control registers
+0x25E4
stencil reference and masks
+0x26B0
depth target, 13 dwords
+0x26E4
5 colour targets, 11 dwords each
+0x27C4
one in-use word per colour target
Every setter skips the call when the cached id or pointer is unchanged. A target is off when the format in its info register is 0. Its base address is then stale.
The context is a complete record of fixed-function draw state. Its copies match the GPU registers on every draw, on the primary and the deferred contexts alike: shaders, colour and depth targets, blend, depth and stencil control, raster mode, clipping and viewport. A deferred context’s command buffers are assumed to inherit the render-target registers from earlier buffers in submit order. The snapshot restore at a job’s start keeps the context’s copies equal to them.
Textures, samplers, buffers and constants are not in that check. They reach the GPU through descriptor tables, not registers.
The constant update engine
Sony’s constant update engine keeps a 48 KB CPU copy of every stage’s resource tables: 7 stages, 0x642 dwords each.
dword offset
table
0x000
texture descriptors, 128 x 8 dwords
0x400
read-write texture descriptors, 16 x 8
0x480
sampler descriptors, 16 x 4
0x4C0
vertex buffer descriptors, 32 x 4
0x540
constant buffer descriptors, 20 x 4
Setters write into it and mark a table dirty. Before each draw, the engine binds changed shaders, then for each dirty table the shader uses it writes the table to constant RAM, dumps it to a ring, and points the shader’s user registers at it. Shaders find their resources through their input-usage slots.
stage numbering
0
1
2
3
4
5
6
7
Gnm stage
CS
PS
VS
GS
ES
HS
LS
engine stage
PS
VS
GS
HS
CS
PS4 only
PS4 only
Engine stages 1 to 7 map to Gnm stages 1, 2, 3, 5, 0, 6 and 4.
Where state comes from
kind
source
textures
the engine texture object starts with its raw descriptor; set by the material bytecode (opcode 0x46) and by technique binds
samplers
a 16-byte descriptor at sampler object +0x08; material opcode 0x47
constant buffers
allocated in the command buffer; filled per draw by the technique bind and the material bytecode
vertex streams
one buffer descriptor per layout element; the vertex shader slot is the element index
shaders
the technique apply
blend, depth-stencil, raster, depth bias
preset tables of fixed rows, turned into registers at bind time
render targets
4 colour targets and a depth target; the viewport comes from their size
The Xbox build builds the same preset rows into D3D state objects on the device at start-up. PS4 converts them at each bind.
Clears
A clear takes a mask. Bits 0x3C clear colour targets, bit 4 << i for target i. Bits 0xC0 clear depth: 0x40 writes depth and 0x80 stencil.
- A colour clear skips empty slots and uses a fast clear where the target allows it. The rest is one rect list drawn with the device’s clear shaders, blend off.
- A depth clear is one rect list with the depth test set to always and stencil replace.
Handles
Engine resources resolve from 32-bit handles through one pool table.
uint32_t s = (int32_t)h >> 13; /* signed shift */
uint32_t pool = s & ((s >> 18) | 0x3FF); /* ids with bit 31 set reach pools up to 0x3FFF */
uint32_t index = h & 0x1FFF;
A pool entry is 64 bytes: base at +0x10, stride at +0x30, mask at +0x34. A vertex buffer object holds its GPU address at +0x28 and its element count at +0x30.
Shaders
A shader is a Gnmx vertex or pixel shader struct. Its header dword holds the size in bits 0 to 22 and a “uses shader resource table” flag in bit 23. The engine vertex shader object wraps it at +0x14, with the fetch shader’s GPU address at +0x08. The shader hardware registers are written inside Sony’s driver module, not in the executable.
What the GPU gives back to the CPU
The engine reads GPU results on the CPU and depends on submission order.
item
fact
frame label
4 bytes in GPU-visible memory, written by an end-of-pipe event at every record close and finish, or only at flips when a global byte is set; the consumer keeps the CPU at most one flip ahead
label slots
512 slots of value and block list. Label values climb by one per slot. A record takes its slot when it begins but enters the ring when it closes, so slot order is not ring order
blitter ring
a CPU-side queue of 2,560 GPU copies of 64 bytes each, used by memory defragmenting, with its own label. It is flushed once a frame, and by a blocking defragment that only runs while the main loop has stopped time
occlusion queries
899 slots with 128-byte result buffers. The only CPU reader found is the lens flares, which poll without blocking. A flare’s visible fraction is its pixel count over the expected count
predication
only in the lens flares: draw if visible, or if the result is not final yet
CPU-readable surfaces
autoexposure, minmax_depth and impulse_*_tex; see below
colour grading table
drawn on the GPU every frame, one draw per slice; no CPU readback found
compute
one dispatch call site; no async compute
flip
an event queue with a GPU end-of-pipe event; the flip status is polled
The occlusion query result is 16 qwords, read as 8 begin and end pairs. It is ready when bit 63 is set in all 16. The count is the sum of end minus begin. The counts come from pixel pipeline statistics dumps. A query’s begin first clears its 128-byte result to zero, then waits for the clear.
CPU-readable surfaces
A surface with recipe flag 8 gets a twin in CPU-readable memory. A copy draws the surface into the twin, so the twin holds the GPU’s tiled layout, not plain rows.
surface
size
read by
autoexposure, surfaces 2 to 4
40 x 1, RGBA16F
exposure; reads the first width 8-byte values
minmax_depth, surfaces 13 and 14
1 x 1, RG32F
the shadow cascade fit; a minimum of 0 or less resets the fit
impulse_*_tex, surfaces 15 to 20
read only when a global flag is set
Both readers expect the GPU to have finished the previous frame. Autoexposure draws into the next of its three surfaces each frame and reads the one drawn the frame before. Shadow fitting draws one of two surfaces and reads the other.
Each exposure step moves at most 5% of the gap, clamped to -1 to 1, and only when the last 6 targets lie on one side. The step runs once a frame, not once a second. So at a higher frame rate, exposure settles faster, for example after a loading screen.
Surface copies below the render API
A surface copy between two different surfaces draws through Gnmx directly, so no context draw records it:
- a depth surface is drawn into the target’s depth memory, and stencil too when both have it;
- a colour surface of the same size, and the same value in one header word, is drawn into the target;
- any other colour copy runs a compute pass. The director’s cursor lens uses the same pass.
In patrol, one depth copy and two colour copies run each frame:
- The depth copy puts scene depth into a 1920 x 1080 R32F surface that 7 later draws sample.
- One colour copy copies the scene colour.
- The other copies a quarter-size particle target. The 6 particle draws after it sample the copy as a 480 x 270 RGBA16F texture.
One orbit frame
One frame in orbit made 5,720 render API calls on four threads.
what
count
draws
282: 116 non-indexed, 112 indexed, 50 UI draws, 4 others
colour grading table draws
33, one per slice
lens flare draws
18 indexed
surface unbinds
131, each followed by a GPU stall
other GPU stalls
41
render jobs
3, on deferred contexts 2, 3 and 4
The primary context passes between three threads. Each piece ends with a record close, which writes a frame label, so the label climbs through the frame rather than once per flip.
At the title screen every draw is a UI draw, about 27 a frame, each with a full state set.
The Xbox build
The Xbox backend pairs with the PS4 render API almost function for function. Both builds reach their backend through the same context field with the same arguments. The Xbox context copies the D3D11 context’s method table into itself and calls through it.
PS4 render API
D3D11 on Xbox
indexed draw
set topology, DrawIndexed; Xbox also passes a base vertex
non-indexed draw
Draw; PS4 has no start vertex
vertex streams, index buffer, input layout
IASetVertexBuffers, IASetIndexBuffer, IASetInputLayout
vertex and pixel shader
VSSetShader, PSSetShader
blend, depth-stencil, raster, depth bias
OMSetBlendState, OMSetDepthStencilState, RSSetState
viewport, render targets, clear
RSSetViewports, OMSetRenderTargets, the clear calls
textures, per-draw constants
*SetShaderResources, a constant buffer from the rings
queries and predication
CreateQuery, Begin, End, GetData, SetPredication
finish a recording, consume in order
FinishCommandList, ExecuteCommandList in ring order
- Only on PS4: compute dispatch, draws from CPU vertices, GPU stalls, primitive restart, depth decompress, fast-clear eliminate, and combined viewport and scissor.
- Only on Xbox: hull, domain, geometry and compute shader setters, stream-out targets, ESRAM discard and mip generation.
- The Xbox constant buffer and sampler binds write D3D11.X fast-path packets straight into the command buffer. Xbox also ends in GCN descriptors.
Rainbow Six Seige
Rainbow Six Seige’s PS4 build from 2020 is a direct descendant of this layer. Rainbow Six Seige PC implements it on D3D11.
- The same device manager, a primary context with a per-thread claim, and a ring of records consumed in order. PS4 has 1 + 6 contexts and a 64-record ring with the same 300 us wait. PC has 1 + 8 contexts and a 128-record ring.
- The same draw shape, topology enum, handle tables and engine buffer object.
- PC builds 90 blend, 83 x 2 depth-stencil and 9 x 9 raster and depth bias states at start-up. PS4 keeps the same 132-byte rows and converts them at bind time.
- PC constants come from one dynamic buffer per stage and size bucket: 7 stages x 21 buckets, from 64 to 2,048 bytes, then 4,096. They are filled and unmapped before the bind. PS4 keeps Destiny 1’s “allocate in the command buffer, return a pointer”.
- PC patches no GPU memory in place. Package textures are immutable. Streaming builds a new texture and swaps it in after 3 frames. PC uses no GPU fence anywhere.
- PC tag types: 12 hull shader, 13 domain shader, 14 compute shader, 17 sampler. On Destiny 1 PS4 the sampler is type 16.
How Rainbow Six Seige PC renders is in Rendering.
Not checked
- Device start-up, the constant update engine’s emission code in full, and the preset tables.
- The material bytecode interpreter beyond opcodes
0x46to0x49. - Who builds input layouts.
- Resource types 7, 11 to 14, 16 and 18 in detail.
- Who predicates outside the lens flares, at a destination.
- Which job uses which deferred context.
- The reader of the general occlusion query controller. Orbit begins 3 queries a frame; their owners were not traced.
- Scissor, primitive restart and the clear values, against the GPU registers.
