Skip to content

Runtime capacities and allocator layouts

Frostbite (skate.)Runtime and limitsApplies toskate.

The capacities below apply to Skate.exe build 20260929. ReSkate changes selected allocations and guards; content counts alone do not describe pressure when stock resident content and outgoing layers still occupy the same pool. Observed

Pool or extent Stock ReSkate behaviour Confidence
Streamed texture handles 20,480 32,764 from 2.0.7 Observed
Cull ids per spatial queue (SpatialQueryManager.InitialCullIdListSize) 40,000, allocated once and never grown 262,144 in 2.1.0 Sourced · ReSkate
Light ids per class 8,192 in each of six classes Guard refuses new light entities near capacity Observed
Physics geometries/bodies 5,000 / 8,200 40,000 / 65,000 from 1.1.7 Sourced · ReSkate
Render-resource ids 8,192 65,536 from 1.1.7 Sourced · ReSkate
Spatial records per cull-id owner (the array at owner +0x330) 65,536, no capacity check Unchanged in 2.1.2: it caps a level below the raised cull-id list Observed
Entity slots Not established here 262,144 in 2.0.7; an older 524,288 log label is stale Sourced · ReSkate
Renderer descriptor heap 131,072 per frame Raised by 2.1.0 Sourced · ReSkate
Mesh streaming pool 532,960 KB Version-dependent sizing Sourced · ReSkate
Spatial root ±10,000 m (Core.WorldSize) Landings checked at x +8,000 and z −8,000 m Observed

The stock handle count is an immediate, mov r12d,0x5000 at Skate+0x3e72bd6; exceeding it logs “Exceeded max streaming texture handle count”. Observed

The texture handle index has fifteen bits. The 32,764 allocation remains below the 32,767 numerical ceiling; raising texture resolution and raising handle count are different operations. No 256-pixel texture-side limit is established for skate. Observed

On a measured Studio-level load under the stock handle limit, roughly 10,400 handles remain for map textures, or around 9,000 with the inspected cosmetic mods. ReSkate’s texture-use counter is a session high-water mark, so it cannot measure current live residency after unloading a map. Observed

One pool owner exists per level or world context; its constructor is Skate+0x489b9a0. The owner holds 0x800 records of 0x90 bytes at +0x70, a free list of u32 next links at +0x80 and a u32 head at +0xc0. Empty head is 0xffffffff. Observed

Exactly three functions pop the list (+0x489cf90, +0x489db30, +0x489d400): they are every read of a head at displacement 0xc0 in the module. The failing pop at Skate+0x489cfe9 indexes with the sentinel, reading [array+0x3fffffffc], instead of rejecting exhaustion. If that read succeeds, the pop writes -1 to wild addresses and hands out garbage ids, so corruption can precede any fault. Observed

The game’s GameView.cfg sets 80,000, but it is read only in GameView mode; ordinary play keeps 40,000. Observed

One spatial/render object holds its id at +0x3c. Grouped instances are counted by member mesh, not instance. Repeated skater creation due to missing spawn ground can exhaust ids independently of the map’s steady count. Observed

The same owner holds a second, paged array at +0x330: 512 page pointers (+0x330 to +0x1330), the page allocator at +0x1330 and a u32 count at +0x1338. A page holds 128 records of 0x30 bytes. The append Skate+0x4eeff50 takes page count>>7, allocates a missing page, returns page + (count & 127) * 0x30 and increments the count, without checking it against the 512 pages. Observed

The registration at Skate+0x4ebecc9 pops a cull id for an object that has none (Skate+0x489db30) and appends one record to this array for it, so the array grows with the cull ids the owner hands out. It holds 65,536 records. Appends at counts 65,536 to 65,663 take the allocator pointer at +0x1330 as page 512 and write their records over the allocator object; the append at count 65,664 takes the qword at +0x1338 (the count, with the next u32 above it) as page 513. Observed ReSkate’s patch tables at 2.1.2 change neighbouring functions’ page shifts but no instruction of Skate+0x4eeff50. Sourced · ReSkate source

In one Studio level loaded under ReSkate 2.1.2, objects needing 100,777 cull ids (one per object per 200 m cell the object’s box meets) faulted 1 s after the level became active. The fault was at Skate+0x4ebecf0, the write of an appended record, through the page pointer 0x400010080 with a zero record offset: the count 0x10080 (65,664) with 4 above it, read as page 513, as the overflow produces. The same level with 49,410 cull ids loaded. So the raised cull-id list does not raise the level’s capacity: the record array still limits a level to 65,536 cull ids, less what stock resident content in the same owner holds. Observed

The six managers are OldPoint, OldSpot, PbrSphere, PbrSpot, PbrTube and PbrRectangular. Each has four selector tables sharing its class allocator. Each selector has 64 page pointers, with 128 eight-byte entries per page. The insert uses page=id>>7, entry=id&127 without a capacity check. Observed

The static constructor Skate+0x4eab1a0 builds the managers and stores each one’s name at manager +0x410; the dispatcher +0x4ec5ee0 switches on light descriptor +8. Observed

Descriptor type Manager Offset in the owner Creator Entity factory Confidence
0 OldPoint +0x60 +0x4f1b560 +0x52d3f50 Observed
1 OldSpot +0x570 +0x4f1b600 +0x52d4480 Observed
2 PbrSphere +0xa80 +0x4f1b740 +0x52d0e00 Observed
3 PbrSpot +0xf90 +0x4f1b7e0 +0x52d1740 Observed
4 PbrTube +0x14a0 +0x4f1b880 +0x52d1d40 Observed
5 PbrRectangular +0x19b0 +0x4f1b6a0 +0x52d2680 Observed

A selector table is built by +0x304a490; +0x304c2d0 always allocates exactly 64 pages of 0x400 bytes. The class allocator +0x3e844f0 pops its free list at +0xe8 or atomically increments the issue counter at +0xf0; the four selectors of a class share it. The insert is +0x4ea3ff0. Observed

Light selector table known fields592 bytes88% known

Runtime memory; offsets are relative to one selector table.

ProvenObservedSourcedInferredUnknownNot described
OffsetSizeTypeFieldConfidenceNotes
0x0512pointer[64]page pointersObserved
0x2008pointersnapshot pointerObserved
0x20872bytesunknownUnknown

Ids 8,192–8,319 can overwrite through the snapshot pointer before id 8,320 faults at Skate+0x4ea4094 (unwind +0x4ea4094 < +0x4f1b844 < +0x4ec5fbd < +0x1959d92 < +0x5304a7a). Refusing a light in its per-kind entity factory avoids later unchecked renderer setters; returning handle zero after insertion has failed does not. Observed

Allocator records are 0x60 bytes. For id i, page is bsr(i+16)-4, its pointer at allocator +8+8*page, and index is (i+16)-2^bsr(i+16); the record lookup is Skate+0x604af0. The next free id is at +0x50. Live count is issued count at allocator +0xf0 minus free-list length. Observed

A light’s creation information carries no owner that would attribute it to a layer: +0x140 is the per-light transform, +0x158 the creation batch, and +0x10/+0x90 are pooled. Observed

ParamDb owner known fields112 bytes27% known

Runtime memory; remaining ranges are not interpreted here.

ProvenObservedSourcedInferredUnknownNot described
OffsetSizeTypeFieldConfidenceNotes
0x08u64reference count regionObserved
0x88pointerallocatorObserved
0x188pointerbinding tableObserved
0x484u32type regionObserved
0x4E2u16member countObserved
0x58variablepointerspayload pointersObserved

The owner’s 40-byte payload block begins with the word (8 << 48) | owner. Observed

The binding table at owner +0x18 reaches its records through a page-pointer directory. The 64-bit word at table +8 packs the directory pointer in its low 48 bits, the directory length in the next 15 bits and a growth lock in the top bit. +0x3cbb6c0 grows the directory on demand, checking the page index against the packed length at +0x3cbb6ef. Inferred

Page j holds 2^(j+1) records of 0x30 bytes, so record n lives in page floor(log2(n+2)) - 1 at index n + 2 - 2^(j+1); callers derive n from per-owner 32-bit counters. These pages grow without a fixed capacity, unlike the 64 × 128 light tables. Inferred

+0x3cb5a30 creates an owner through the constructor +0x3cb5dc0, which always makes a new owner rather than reusing one. Observed

The owner and its payload are freed synchronously the moment the reference count reaches zero. About 680 open-coded call sites decrement and free in place (251 through +0x3cba570, 419 through the value-box thunk +0x3cba640, others through +0x3cba7a0 and +0x3cb4d00), with no lock and no deferred phase. Observed

GlobalSBA’s free path (+0x15a5c20) writes the previous free block into the freed allocation’s first qword as a per-thread LIFO link. Consequently a stale reference can appear as a plausible allocator pointer: a block whose first qword points at another free block of the same size has itself been freed. Observed

Each ParamDb manager has a job queue. +0x3cbcfe0 reserves it, enqueuing a marker only when the queue is empty; +0x3cbea30 releases and drains it; the enqueue +0x18b6180 returns true only for an empty queue. Observed

Job descriptor Job Function Confidence
+0x7181208 Walker +0x3cbcd30 Observed
+0x7181280 Batch walk +0x3cbabd0 Observed
+0x7181230 Destroy +0x3cb37a0 Observed
+0x7181258 Marker Not named Observed

The walker holds no references to the owners it visits, and the creator +0x3cb9d00 publishes an owner before taking references to it; both are safe only inside a queue phase. Observed The scoped arenas +0x4898f70, +0x4899ae0 and +0x48994c0 are per-call scratch. Old binding-table page directories retire through a four-slot epoch ring at +0x771f470, drained by +0x3f7bf10. Observed

Loaded ShaderBlockDepots register each material key in a hash table of ParamDb value boxes. The depot holds the bucket array pointer at +0x1d0 and the bucket count at +0x1d8; buckets[count] is a sentinel. The insert helper +0xfa55d0, called at +0x46a00b4, returns the node, its bucket, and an inserted flag at result +16. Observed

Depot key registry node32 bytes88% known

Runtime memory.

ProvenObservedSourcedInferredUnknownNot described
OffsetSizeTypeFieldConfidenceNotes
0x08u64material keyObserved
0x88pointervalue box handleObservedPoints at the box's 64-bit reference count
0x104u32registration countObserved
0x144bytesunknownUnknown
0x188pointernext nodeObserved

The handle points at the reference count, eight bytes into the box; the release thunk +0x3cba640 subtracts eight and continues at +0x3cba7a0. During one measured load of a level whose depots register different blocks under one key, a key was registered twice with a box other than the node’s. Observed

+0x1581010 frees with an explicit arena, pointer and size; +0x157dae0 frees by pointer alone. Its type-7 case (jump table at +0x15812a4) calls +0x159c970, which resolves the actual arena and re-enters +0x1581010 with the same pointer and size at +0x159c9ab. A hook on that function therefore records two frees for one logical free; the nested one is not a second allocation. Observed

The arena object at +0x7319ad0 is GlobalSBA, initialised by +0x1599220. Observed

The DX12 backend is linked into Skate.exe; D3D12CreateDevice is imported through sl.interposer.dll, and the game loads the Agility runtime from x64/D3D12Core.dll. Missing that runtime produces error 0x887e0003. Observed

GPU-hang reports include an RC2 breadcrumb tree, the game’s own DRED dump, camera and memory information. They are plain protobuf files named eadp.errorsdata.<UTC>_gpu.st.dat in eadp-sdk/persistence/PROD of the game’s Electronic Arts application-data folder, and the same text is emitted through OutputDebugString. In eight of nine recorded hangs the breadcrumb sits in GBuffer_HighEnd > mainGBufferEs. Code resides in executable .xtls/.tls$ sections as well as ordinary PE structures, so scanning only .text misses relevant functions. Observed

Loose config/Game.cfg is read at startup; a line applySettings [=[ Name value ]=] appended to it sets an engine setting for the run. The settings catalogue (6,602 settings) can expose values that exist but have no visible effect: PerfOverlay enable/FPS settings read back as one without drawing in this build, and FovMultiplier changes nothing in the frame. Mesh.ForceLod 1 or 2 does force mesh LODs. Observed

An unfocused game window presents at about 23 frames per second wherever the camera is, so frame-rate measurements need a focused window. Observed

Frame rate also falls over a session. Players report a decline from 150 frames per second at half GPU load to about 20 at full load within one session, and custom maps dropping to about 10–40 frames per second after another map is loaded in game, which stock maps do not; only restarting the game restores it. Sourced · ReSkate community reports Frame-rate comparisons therefore start from a fresh launch. Inferred

Live values read at the main menu at the Ultra quality tier:

Setting Value Confidence
WorldPartition.AgentFloatAttribute00–03 (fullagent long/medium/short/detail simulation radius) 230 / 130 / 80 / 60 m Observed
WorldPartition.AgentFloatAttribute04, 05 (long/medium vista radius) 10,000 / 10,000 m Observed
WorldRender.DetailCullDistance, MediumCullDistance, LongCullDistance, ExtraLongCullDistance 60 / 130 / 230 / 400 m Observed
WorldRender.CullScreenAreaScale 1.75 Observed
WorldRender.ShadowmapViewDistance 250 Observed
Render.ViewDistance 20,000 Observed
Mesh.GlobalLodScale, Mesh.ForceLod, Mesh.RuntimeLodQualityLevel 1, −1, 3; no LodScale setting exists Observed
MeshStreaming.PoolMaxAllocCount, MaxPendingLoadCount 2,048, 4 Observed
MeshStreaming.ReservedPositionedInstanceCount 50,000; a written 200,000 is applied Observed
MeshStreaming.ReservedDistancedInstanceCount 128 Observed
GpuRp.MaxVisibleMeshlets 1,048,576 Observed
TextureStreaming.PoolSize 1,441,040 Observed

Placed-object cull defaults are Near 70 m, Far 200 m, FarSp 350 m and AreaFactor 0.038; values written to them take effect and survive a level load. Observed MeshStreamingSettings also carries debug switches (DumpInstanceList, TraceMeshInstanceCounts, DumpPoolAllocations) and Sweepable* pool settings, whose effect is not measured. Observed