Files
AyeZeeBB a83cb83bb4 Raise the streamed texture limit to 32764, and warn when it is close
The game can stream 20480 textures at once. When a map and its mods
need one more, it takes a slot past the end of its table and waits for
ever on a byte that is part of a counter: a hang, then a crash in the
hang's own report (crash reports of 2026-10-11).

The limit is raised to 32764, the most a texture's 16-bit handle can
index, before the game makes its texture streaming manager:

- The manager is allocated longer (6.0 MB, was 2.3 MB) and its slot
  table and per-slot floats moved behind its end. Each of the 226
  instructions that hold one of those offsets or sizes gets another
  number; no code is added. Its four fixed lists are pointed at longer
  storage once the constructor has run.
- The renderer keeps a block of floats per slot for each of four
  layers. Its object grows to match (27 instructions), and the two
  places that index a layer's block as layer * 20480 become a multiply
  by the new count, in the same twelve bytes.
- Three functions keep 20480 entries on their own stack. Two list
  filters go through a longer list a stretch at a time, and the sort
  is given scratch room of its own past that many. Up to 20480 they
  run as the game wrote them.

Every instruction is checked byte for byte first, and nothing is
changed if any differs or the manager already exists. An empty
texture_limit_stock.txt beside the game leaves the limit as it is.

Players are told when they come close: a notice at 90% of the limit
in force and an error at 100%, each once until use falls below 80%.
The log records each new high in steps of 5%.

Also: when a function of the game finds its stack written over, the
game ends itself with exit code 0xc0000409 and no crash report. The
log now names that function and its callers first.

The site table (texture_streaming_sites.inc) is generated from a sweep
of the supported build; texture_streaming.h describes the layout.

Tested in game below 20480 textures only. Nothing has run above the
old limit, where the three stack guards and the longer lists first
matter. At 32764 the game still hangs; the notice is the protection.
2026-10-11 19:26:23 -05:00

163 lines
9.8 KiB
C++

#pragma once
#include "Engine/Game/Build/fingerprint.h"
#include <array>
#include <cstdint>
// The game's TextureStreamingManager and its limit of streamed textures
// (Extension/World/texture_handle_limit.cpp).
//
// Every streamed texture holds a 16-bit handle: an index into the manager's table of slots
// (0x58 bytes each), 0xffff for none, and the code masks the index with 0x7fff. The table and
// what goes with it are sized for fb::MaxStreamingTextureCount, 0x5000 (20480), all inside the
// manager itself (one object of 0x24e780 bytes, FUN_143efd770 allocates it and keeps it at
// `manager`):
// +0x480 a list of pointers, its 0x5000 entries at +0x498
// +0x28498 the slots
// +0x1e0498 how many slots are taken from the bottom (one kind of texture), +0x1e049c where
// those taken from the top begin (the other kind; 0x5000 when none), and a hint
// for each at +0x1e04a0 and +0x1e04a4
// +0x1e0500 a float for each slot
// +0x1f4500 two lists of pointers, 0x5000 entries each: headers at +0x1f4500 and +0x21c518,
// entries behind each header
// +0x244530 a sorted list of handles, its 0x5000 entries at +0x244548
// The lists are {begin, end, capacity end}; only the constructor names where their entries
// are. When the slots from the bottom and from the top meet, FUN_143e72660 writes a message
// ("Exceeded max streaming texture handle count") nobody reads and takes the next slot all the
// same: one past the table, which is the counter at +0x1e0498, whose low byte it then waits on
// as a lock for ever (a stall and then a crash in the stall's own report, 2026-10-11).
//
// The limit is raised by giving the manager a longer allocation and moving the slots, the
// floats and the lists' entries behind its end: every instruction that holds one of those
// offsets or sizes is in texture_streaming_sites.inc (a sweep of the build's code, checked two
// ways; nothing outside 0x3e5b350..0x3e77518 and the allocation holds one), and each is the
// same instruction with another number. The lists are pointed at their new entries once the
// constructor has run. The counters, the hints and everything else stay where they are.
//
// The renderer keeps floats of its own for every slot: an object for each view (0x500c0 bytes,
// made at 0x3f3d83a) with a block of 0x5000 floats for each of four layers from +0x40, a
// {pointer, count} for each block from +0x50040, and a pointer at +0x50080. It fills each
// block for the manager's count of slots (FUN_143e6c8e0 hands the view the manager's floats and
// that count), so the blocks have to grow with it: raising the manager alone ran the fill over
// the pointer behind the blocks (a crash at 0x3f31d2c on the first frame, 2026-10-11). Three
// functions go into the blocks: FUN_143f31aa0 (the fill) and FUN_143f319c0 (the merge into the
// manager's floats), both by layer * 0x5000 + 0x10 floats, written as lea [r + r*4], shl 12,
// add 0x10; and FUN_143f56560 (the jobs that write them), by the {pointer, count} of its
// layer, found as (layer + 0x5004) * 16. Nothing else in the code has that arithmetic. The
// blocks become raised_count floats each, the fields behind them move up, and the two
// lea/shl/add become an imul and the same add (renderer_rewrites).
namespace dingosdk::game::build::v20260929::texture_streaming {
inline constexpr std::uintptr_t manager = 0x76e92e0; // TextureStreamingManager*, null until made
inline constexpr Fingerprint constructor{0x3e5b350, {
0x48,0x89,0x5c,0x24,0x18,0x55,0x56,0x57,0x41,0x54,0x41,0x55,0x41,0x56,0x41,0x57,
0x48,0x8d,0xac,0x24,0x20,0xff,0xff,0xff,0x48,0x81,0xec,0xe0,0x01,0x00,0x00,0x48}};
// __report_gsfailure: mov ecx, 2; int 0x29; ret. Every function's stack check jumps to it
// (FUN_14546ba30) when its cookie is not what it left (Extension/World/stack_check_report.cpp).
inline constexpr std::uintptr_t stack_check_failed = 0x546be9c;
inline constexpr std::array<unsigned char, 8> stack_check_failed_bytes{0xb9, 0x02, 0x00, 0x00, 0x00, 0xcd, 0x29, 0xc3};
inline constexpr std::uint32_t stock_count = 0x5000;
// The most the 15-bit index allows, a multiple of four (the floats are reset sixteen bytes at
// a time) and short of 0x7fff, which with the index's high bit set would read as no handle.
inline constexpr std::uint32_t raised_count = 0x7ffc;
inline constexpr std::uint32_t slot_size = 0x58;
inline constexpr std::uint32_t taken_from_bottom = 0x1e0498, taken_from_top = 0x1e049c;
inline constexpr std::uint32_t stock_size = 0x24e780;
inline constexpr std::uint32_t stock_slots = 0x28498, stock_floats = 0x1e0500;
// Behind the stock object, in this order.
inline constexpr std::uint32_t raised_slots = stock_size;
inline constexpr std::uint32_t raised_slot_bytes = raised_count * slot_size;
inline constexpr std::uint32_t raised_slots_end = raised_slots + raised_slot_bytes;
inline constexpr std::uint32_t raised_floats = raised_slots_end;
inline constexpr std::uint32_t raised_float_bytes = raised_count * 4;
struct List {
std::uint32_t header, stock_entries, raised_entries, entry_size;
};
inline constexpr std::uint32_t lists_begin = raised_floats + raised_float_bytes;
inline constexpr List lists[]{
{0x480, 0x498, lists_begin, 8},
{0x1f4500, 0x1f4518, lists_begin + raised_count * 8, 8},
{0x21c518, 0x21c530, lists_begin + raised_count * 16, 8},
{0x244530, 0x244548, lists_begin + raised_count * 24, 2},
};
inline constexpr std::uint32_t raised_size = (lists_begin + raised_count * 26 + 0x7f) & ~0x7fu;
static_assert(raised_slots % 8 == 0 && raised_floats % 4 == 0 && lists_begin % 8 == 0 && raised_float_bytes % 16 == 0);
// The renderer's object: four blocks of floats from +0x40, then its fields.
inline constexpr std::uint32_t renderer_blocks = 0x40;
inline constexpr std::uint32_t renderer_stock_fields = renderer_blocks + 4 * stock_count * 4; // 0x50040
inline constexpr std::uint32_t renderer_raised_fields = renderer_blocks + 4 * raised_count * 4; // 0x80000
static_assert(renderer_stock_fields == 0x50040 && renderer_raised_fields % 16 == 0);
// layer * count + 0x10, in floats: twelve bytes either way.
struct Rewrite {
std::uint32_t rva;
std::uint8_t stock[12], raised[12];
};
inline constexpr Rewrite renderer_rewrites[]{
// lea r8, [r10 + r10*4]; shl r8, 12; add r8, 0x10 -> imul r8, r10, raised_count; add r8, 0x10; nop
{0x3f31a21, {0x4f, 0x8d, 0x04, 0x92, 0x49, 0xc1, 0xe0, 0x0c, 0x49, 0x83, 0xc0, 0x10},
{0x4d, 0x69, 0xc2, raised_count & 0xff, raised_count >> 8, 0, 0, 0x49, 0x83, 0xc0, 0x10, 0x90}},
// lea rdx, [rax + rax*4]; shl rdx, 12; add rdx, 0x10 -> imul rdx, rax, raised_count; add rdx, 0x10; nop
{0x3f31cfa, {0x48, 0x8d, 0x14, 0x80, 0x48, 0xc1, 0xe2, 0x0c, 0x48, 0x83, 0xc2, 0x10},
{0x48, 0x69, 0xd0, raised_count & 0xff, raised_count >> 8, 0, 0, 0x48, 0x83, 0xc2, 0x10, 0x90}},
};
// Three functions of the manager keep stock_count entries on their own stack, checked or not:
// - FUN_143e585a0 and FUN_143e58770 (manager, list*): each goes through one of the two lists of
// pointers, keeps the entries still to be done in a stack array and writes them back. Past
// the array they write to null (0xdeadc0de).
// - FUN_143e58950 (begin, end, scratch): a radix sort of the entries of the list at +0x480
// (eight bytes each, a float and a handle), called only by FUN_143e753c0 with a stack array as
// its scratch room, unchecked.
inline constexpr Fingerprint filter_loading{0x3e585a0, {
0x48,0x89,0x5c,0x24,0x18,0x48,0x89,0x6c,0x24,0x20,0x56,0x57,0x41,0x56,0xb8,0x40,
0x80,0x02,0x00,0xe8,0xa8,0x34,0x61,0x01,0x48,0x2b,0xe0,0x48,0x8b,0x05,0xfe,0xbd}};
inline constexpr Fingerprint filter_waiting{0x3e58770, {
0x40,0x57,0xb8,0x50,0x80,0x02,0x00,0xe8,0xe4,0x32,0x61,0x01,0x48,0x2b,0xe0,0x48,
0x8b,0x05,0x3a,0xbc,0x36,0x03,0x48,0x33,0xc4,0x48,0x89,0x84,0x24,0x38,0x80,0x02}};
inline constexpr Fingerprint sort_wanted{0x3e58950, {
0x4c,0x8b,0xdc,0x53,0x56,0x41,0x54,0x48,0x81,0xec,0x40,0x08,0x00,0x00,0x48,0x8b,
0xc2,0x49,0x8b,0xf0,0x48,0x2b,0xc1,0x48,0x8b,0xda,0x48,0x83,0xe0,0xf8,0x4c,0x8b}};
// What the 32-bit value of a site is, and so what it becomes.
enum class Value : std::uint8_t {
slot, // an offset into the first slot: moved with the slots
slot_bytes, // the size of all the slots
slot_end, // the end of the slots (the destructor walks them backwards from it)
count, // the count of slots
floats, // the floats' offset
float_bytes, // the size of all the floats
alloc, // the manager's size, where it is allocated and where it is freed
r_alloc, // the renderer's object: its size
r_fill, // the floats of its four blocks together
r_block, // where its second, third and fourth block begin
r_field, // a field behind its blocks
r_spans, // those fields' offset in sixteens (the {pointer, count} of a layer)
};
struct Site {
std::uint32_t rva;
std::uint8_t size, at;
Value value;
std::uint8_t bytes[15];
};
inline constexpr Site sites[]{
#include "texture_streaming_sites.inc"
};
constexpr std::uint32_t raised(Value value, std::uint32_t stock) {
switch (value) {
case Value::slot: return stock - stock_slots + raised_slots;
case Value::slot_bytes: return raised_slot_bytes;
case Value::slot_end: return raised_slots_end;
case Value::count: return raised_count;
case Value::floats: return stock - stock_floats + raised_floats;
case Value::float_bytes: return raised_float_bytes;
case Value::alloc: return raised_size;
case Value::r_alloc: return stock - renderer_stock_fields + renderer_raised_fields;
case Value::r_fill: return raised_count * 4;
case Value::r_block: return renderer_blocks + (stock - renderer_blocks) / (stock_count * 4) * (raised_count * 4);
case Value::r_field: return stock - renderer_stock_fields + renderer_raised_fields;
case Value::r_spans: return renderer_raised_fields / 16;
}
return stock;
}
}