mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-19 03:15:21 +02:00
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.
163 lines
9.8 KiB
C++
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;
|
|
}
|
|
}
|