diff --git a/Engine/Game/Build/20260929/texture_streaming.h b/Engine/Game/Build/20260929/texture_streaming.h new file mode 100644 index 0000000..40c8000 --- /dev/null +++ b/Engine/Game/Build/20260929/texture_streaming.h @@ -0,0 +1,162 @@ +#pragma once +#include "Engine/Game/Build/fingerprint.h" +#include +#include + +// 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 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; +} +} diff --git a/Engine/Game/Build/20260929/texture_streaming_sites.inc b/Engine/Game/Build/20260929/texture_streaming_sites.inc new file mode 100644 index 0000000..a553b1f --- /dev/null +++ b/Engine/Game/Build/20260929/texture_streaming_sites.inc @@ -0,0 +1,259 @@ +// Generated by a sweep of the supported build (see texture_streaming.h): do not edit by hand. +// {rva, length, offset of the 32-bit value, what the value is, the instruction's bytes} +{0x3e5b44a, 5, 1, Value::count, {0xb9,0x00,0x50,0x00,0x00}}, // mov ecx, 0x5000 +{0x3e5b46c, 7, 3, Value::slot, {0x48,0x8d,0x86,0x98,0x84,0x02,0x00}}, // lea rax, [rsi + 0x28498] +{0x3e5b49b, 7, 3, Value::floats, {0x48,0x8d,0x86,0x00,0x05,0x1e,0x00}}, // lea rax, [rsi + 0x1e0500] +{0x3e5b98f, 7, 3, Value::slot, {0x48,0x8d,0x8e,0x98,0x84,0x02,0x00}}, // lea rcx, [rsi + 0x28498] +{0x3e5b996, 6, 2, Value::slot_bytes, {0x41,0xb8,0x00,0x80,0x1b,0x00}}, // mov r8d, 0x1b8000 +{0x3e5d2f0, 5, 1, Value::count, {0xbf,0x00,0x50,0x00,0x00}}, // mov edi, 0x5000 +{0x3e5d2f5, 7, 3, Value::slot_end, {0x48,0x8d,0xb3,0x98,0x04,0x1e,0x00}}, // lea rsi, [rbx + 0x1e0498] +{0x3e5f2e9, 5, 1, Value::alloc, {0xba,0x80,0xe7,0x24,0x00}}, // mov edx, 0x24e780 +{0x3e620fb, 8, 3, Value::slot, {0x80,0xbc,0x30,0xa7,0x84,0x02,0x00,0x00}}, // cmp byte ptr [rax + rsi + 0x284a7], 0 +{0x3e62105, 8, 4, Value::slot, {0x48,0x8b,0x84,0x30,0xb0,0x84,0x02,0x00}}, // mov rax, qword ptr [rax + rsi + 0x284b0] +{0x3e6247f, 8, 3, Value::slot, {0x80,0xbc,0x31,0x9c,0x84,0x02,0x00,0x00}}, // cmp byte ptr [rcx + rsi + 0x2849c], 0 +{0x3e627de, 6, 2, Value::count, {0x81,0xfa,0x00,0x50,0x00,0x00}}, // cmp edx, 0x5000 +{0x3e627f6, 8, 4, Value::slot, {0x44,0x39,0x84,0x19,0xdc,0x84,0x02,0x00}}, // cmp dword ptr [rcx + rbx + 0x284dc], r8d +{0x3e62800, 9, 4, Value::slot, {0x48,0x83,0xbc,0x19,0xa8,0x84,0x02,0x00,0x00}}, // cmp qword ptr [rcx + rbx + 0x284a8], 0 +{0x3e62811, 6, 2, Value::count, {0x81,0xfa,0x00,0x50,0x00,0x00}}, // cmp edx, 0x5000 +{0x3e631ff, 8, 3, Value::slot, {0x80,0x8c,0x01,0xa7,0x84,0x02,0x00,0x01}}, // or byte ptr [rcx + rax + 0x284a7], 1 +{0x3e63246, 8, 4, Value::slot, {0x41,0x0f,0xb7,0x91,0xe4,0x84,0x02,0x00}}, // movzx edx, word ptr [r9 + 0x284e4] +{0x3e63286, 8, 4, Value::slot, {0x41,0x0f,0xb7,0x91,0xe6,0x84,0x02,0x00}}, // movzx edx, word ptr [r9 + 0x284e6] +{0x3e632d5, 9, 5, Value::slot, {0x43,0x0f,0xb7,0x94,0x3b,0xe4,0x84,0x02,0x00}}, // movzx edx, word ptr [r11 + r15 + 0x284e4] +{0x3e6331c, 9, 5, Value::slot, {0x43,0x0f,0xb7,0x94,0x3b,0xe6,0x84,0x02,0x00}}, // movzx edx, word ptr [r11 + r15 + 0x284e6] +{0x3e6336f, 9, 5, Value::slot, {0x43,0x0f,0xb6,0x94,0x39,0x99,0x84,0x02,0x00}}, // movzx edx, byte ptr [r9 + r15 + 0x28499] +{0x3e6337f, 8, 4, Value::slot, {0x43,0x2a,0x8c,0x39,0x9a,0x84,0x02,0x00}}, // sub cl, byte ptr [r9 + r15 + 0x2849a] +{0x3e6339e, 9, 5, Value::slot, {0x43,0x0f,0xb6,0x84,0x39,0x99,0x84,0x02,0x00}}, // movzx eax, byte ptr [r9 + r15 + 0x28499] +{0x3e633a7, 8, 4, Value::slot, {0x4b,0x8b,0xb4,0x39,0xa8,0x84,0x02,0x00}}, // mov rsi, qword ptr [r9 + r15 + 0x284a8] +{0x3e633d4, 8, 4, Value::slot, {0x42,0x86,0x84,0x3b,0x98,0x84,0x02,0x00}}, // xchg byte ptr [rbx + r15 + 0x28498], al +{0x3e633e7, 8, 4, Value::slot, {0x42,0x86,0x84,0x3b,0x98,0x84,0x02,0x00}}, // xchg byte ptr [rbx + r15 + 0x28498], al +{0x3e633f3, 8, 4, Value::slot, {0x4a,0x39,0xb4,0x3b,0xa8,0x84,0x02,0x00}}, // cmp qword ptr [rbx + r15 + 0x284a8], rsi +{0x3e633fd, 9, 5, Value::slot, {0x42,0x0f,0xb6,0x94,0x3b,0x9a,0x84,0x02,0x00}}, // movzx edx, byte ptr [rbx + r15 + 0x2849a] +{0x3e63406, 7, 3, Value::slot, {0x49,0x8d,0x8f,0xb0,0x84,0x02,0x00}}, // lea rcx, [r15 + 0x284b0] +{0x3e6341f, 9, 4, Value::slot, {0x42,0xc6,0x84,0x3b,0x98,0x84,0x02,0x00,0x00}}, // mov byte ptr [rbx + r15 + 0x28498], 0 +{0x3e6779b, 7, 3, Value::slot, {0x49,0x8b,0x8e,0xa8,0x84,0x02,0x00}}, // mov rcx, qword ptr [r14 + 0x284a8] +{0x3e677ab, 7, 3, Value::slot, {0x49,0x8b,0x96,0xa8,0x84,0x02,0x00}}, // mov rdx, qword ptr [r14 + 0x284a8] +{0x3e677b5, 8, 4, Value::slot, {0x45,0x0f,0xb6,0xbe,0x99,0x84,0x02,0x00}}, // movzx r15d, byte ptr [r14 + 0x28499] +{0x3e6780c, 7, 3, Value::slot, {0x49,0x8d,0x96,0xdc,0x84,0x02,0x00}}, // lea rdx, [r14 + 0x284dc] +{0x3e67837, 7, 3, Value::slot, {0x4d,0x8b,0x8e,0xa8,0x84,0x02,0x00}}, // mov r9, qword ptr [r14 + 0x284a8] +{0x3e67858, 8, 4, Value::slot, {0x45,0x0f,0xb6,0x86,0x99,0x84,0x02,0x00}}, // movzx r8d, byte ptr [r14 + 0x28499] +{0x3e67860, 7, 3, Value::slot, {0x49,0x8b,0x96,0xa8,0x84,0x02,0x00}}, // mov rdx, qword ptr [r14 + 0x284a8] +{0x3e678c7, 8, 4, Value::slot, {0x41,0x0f,0xb6,0x86,0x9c,0x84,0x02,0x00}}, // movzx eax, byte ptr [r14 + 0x2849c] +{0x3e68f46, 8, 4, Value::slot, {0x4c,0x39,0xa4,0x30,0xa8,0x84,0x02,0x00}}, // cmp qword ptr [rax + rsi + 0x284a8], r12 +{0x3e68f83, 7, 3, Value::slot, {0x86,0x84,0x37,0x98,0x84,0x02,0x00}}, // xchg byte ptr [rdi + rsi + 0x28498], al +{0x3e68f95, 7, 3, Value::slot, {0x86,0x84,0x37,0x98,0x84,0x02,0x00}}, // xchg byte ptr [rdi + rsi + 0x28498], al +{0x3e68fa4, 9, 5, Value::slot, {0xc5,0xfc,0x11,0x84,0x37,0x99,0x84,0x02,0x00}}, // vmovups ymmword ptr [rdi + rsi + 0x28499], ymm0 +{0x3e68fad, 9, 5, Value::slot, {0xc5,0xfc,0x11,0x84,0x37,0xb9,0x84,0x02,0x00}}, // vmovups ymmword ptr [rdi + rsi + 0x284b9], ymm0 +{0x3e68fb6, 9, 5, Value::slot, {0xc5,0xfc,0x11,0x84,0x37,0xd0,0x84,0x02,0x00}}, // vmovups ymmword ptr [rdi + rsi + 0x284d0], ymm0 +{0x3e68fbf, 10, 4, Value::slot, {0x66,0xc7,0x84,0x37,0xe8,0x84,0x02,0x00,0xff,0xff}}, // mov word ptr [rdi + rsi + 0x284e8], 0xffff +{0x3e68fcf, 7, 3, Value::slot, {0x89,0x84,0x37,0xdc,0x84,0x02,0x00}}, // mov dword ptr [rdi + rsi + 0x284dc], eax +{0x3e68fdb, 8, 4, Value::slot, {0x4c,0x89,0xb4,0x37,0xa8,0x84,0x02,0x00}}, // mov qword ptr [rdi + rsi + 0x284a8], r14 +{0x3e68ff6, 7, 3, Value::slot, {0x88,0x84,0x37,0x9a,0x84,0x02,0x00}}, // mov byte ptr [rdi + rsi + 0x2849a], al +{0x3e69001, 7, 3, Value::slot, {0x88,0x84,0x37,0x9b,0x84,0x02,0x00}}, // mov byte ptr [rdi + rsi + 0x2849b], al +{0x3e69014, 7, 3, Value::slot, {0x88,0x84,0x37,0x99,0x84,0x02,0x00}}, // mov byte ptr [rdi + rsi + 0x28499], al +{0x3e69027, 7, 3, Value::slot, {0x88,0x84,0x37,0x9d,0x84,0x02,0x00}}, // mov byte ptr [rdi + rsi + 0x2849d], al +{0x3e6903a, 7, 3, Value::slot, {0x88,0x84,0x37,0x9e,0x84,0x02,0x00}}, // mov byte ptr [rdi + rsi + 0x2849e], al +{0x3e6904d, 8, 3, Value::slot, {0x80,0xa4,0x37,0xa7,0x84,0x02,0x00,0xfe}}, // and byte ptr [rdi + rsi + 0x284a7], 0xfe +{0x3e69055, 8, 3, Value::slot, {0x80,0x8c,0x37,0xa7,0x84,0x02,0x00,0x90}}, // or byte ptr [rdi + rsi + 0x284a7], 0x90 +{0x3e6905d, 7, 3, Value::slot, {0x88,0x84,0x37,0x9f,0x84,0x02,0x00}}, // mov byte ptr [rdi + rsi + 0x2849f], al +{0x3e6906e, 8, 4, Value::slot, {0x48,0x89,0x84,0x37,0xb0,0x84,0x02,0x00}}, // mov qword ptr [rdi + rsi + 0x284b0], rax +{0x3e69077, 8, 4, Value::slot, {0x44,0x88,0xa4,0x37,0x98,0x84,0x02,0x00}}, // mov byte ptr [rdi + rsi + 0x28498], r12b +{0x3e69474, 8, 4, Value::slot, {0x46,0x39,0xb4,0x3e,0xdc,0x84,0x02,0x00}}, // cmp dword ptr [rsi + r15 + 0x284dc], r14d +{0x3e6948f, 9, 5, Value::slot, {0x42,0x0f,0xb6,0x84,0x3e,0xa7,0x84,0x02,0x00}}, // movzx eax, byte ptr [rsi + r15 + 0x284a7] +{0x3e69563, 7, 3, Value::slot, {0x49,0x8d,0xbf,0x98,0x84,0x02,0x00}}, // lea rdi, [r15 + 0x28498] +{0x3e6956d, 7, 3, Value::slot_bytes, {0x48,0x8d,0xb7,0x00,0x80,0x1b,0x00}}, // lea rsi, [rdi + 0x1b8000] +{0x3e69f3f, 8, 3, Value::slot, {0x80,0xa4,0x01,0xa7,0x84,0x02,0x00,0xfe}}, // and byte ptr [rcx + rax + 0x284a7], 0xfe +{0x3e69f47, 12, 4, Value::slot, {0x48,0xc7,0x84,0x01,0xc8,0x84,0x02,0x00,0x00,0x00,0x00,0x00}}, // mov qword ptr [rcx + rax + 0x284c8], 0 +{0x3e69fa5, 7, 3, Value::slot, {0x48,0x8d,0x96,0xdc,0x84,0x02,0x00}}, // lea rdx, [rsi + 0x284dc] +{0x3e69ffc, 7, 3, Value::slot, {0x48,0x89,0x86,0xb8,0x84,0x02,0x00}}, // mov qword ptr [rsi + 0x284b8], rax +{0x3e6a00d, 7, 3, Value::slot, {0x0f,0xb6,0x86,0x99,0x84,0x02,0x00}}, // movzx eax, byte ptr [rsi + 0x28499] +{0x3e6a016, 6, 2, Value::slot, {0x88,0x8e,0x9c,0x84,0x02,0x00}}, // mov byte ptr [rsi + 0x2849c], cl +{0x3e6a01c, 6, 2, Value::slot, {0x88,0x86,0x9d,0x84,0x02,0x00}}, // mov byte ptr [rsi + 0x2849d], al +{0x3e6a022, 6, 2, Value::slot, {0x89,0x9e,0xa0,0x84,0x02,0x00}}, // mov dword ptr [rsi + 0x284a0], ebx +{0x3e6a048, 7, 3, Value::slot, {0x48,0x8b,0x96,0xa8,0x84,0x02,0x00}}, // mov rdx, qword ptr [rsi + 0x284a8] +{0x3e6a076, 7, 3, Value::slot, {0x0f,0xb6,0x86,0xa7,0x84,0x02,0x00}}, // movzx eax, byte ptr [rsi + 0x284a7] +{0x3e6a0ce, 8, 4, Value::slot, {0x0f,0xb6,0x84,0x29,0x99,0x84,0x02,0x00}}, // movzx eax, byte ptr [rcx + rbp + 0x28499] +{0x3e6a0d6, 8, 4, Value::slot, {0x4c,0x8b,0xa4,0x29,0xa8,0x84,0x02,0x00}}, // mov r12, qword ptr [rcx + rbp + 0x284a8] +{0x3e6a108, 7, 3, Value::slot, {0x86,0x84,0x2b,0x98,0x84,0x02,0x00}}, // xchg byte ptr [rbx + rbp + 0x28498], al +{0x3e6a127, 7, 3, Value::slot, {0x86,0x84,0x2b,0x98,0x84,0x02,0x00}}, // xchg byte ptr [rbx + rbp + 0x28498], al +{0x3e6a132, 8, 4, Value::slot, {0x4c,0x39,0xa4,0x2b,0xa8,0x84,0x02,0x00}}, // cmp qword ptr [rbx + rbp + 0x284a8], r12 +{0x3e6a13c, 8, 4, Value::slot, {0x0f,0xb6,0x94,0x2b,0x9a,0x84,0x02,0x00}}, // movzx edx, byte ptr [rbx + rbp + 0x2849a] +{0x3e6a153, 7, 3, Value::slot, {0x48,0x8d,0x8d,0xb0,0x84,0x02,0x00}}, // lea rcx, [rbp + 0x284b0] +{0x3e6a166, 8, 3, Value::slot, {0xc6,0x84,0x2b,0x98,0x84,0x02,0x00,0x00}}, // mov byte ptr [rbx + rbp + 0x28498], 0 +{0x3e6a200, 7, 3, Value::slot, {0x48,0x8b,0x9e,0xd0,0x84,0x02,0x00}}, // mov rbx, qword ptr [rsi + 0x284d0] +{0x3e6a229, 6, 2, Value::slot, {0xfe,0x8e,0xa6,0x84,0x02,0x00}}, // dec byte ptr [rsi + 0x284a6] +{0x3e6a23c, 11, 3, Value::slot, {0x48,0xc7,0x86,0xd0,0x84,0x02,0x00,0x00,0x00,0x00,0x00}}, // mov qword ptr [rsi + 0x284d0], 0 +{0x3e6a247, 10, 2, Value::slot, {0xc7,0x86,0xd8,0x84,0x02,0x00,0x00,0x00,0x00,0x00}}, // mov dword ptr [rsi + 0x284d8], 0 +{0x3e6a7ef, 8, 3, Value::slot, {0xf6,0x84,0x38,0xa7,0x84,0x02,0x00,0x02}}, // test byte ptr [rax + rdi + 0x284a7], 2 +{0x3e6a7fd, 8, 3, Value::slot, {0x80,0xbc,0x38,0xa6,0x84,0x02,0x00,0x00}}, // cmp byte ptr [rax + rdi + 0x284a6], 0 +{0x3e6a83a, 6, 2, Value::slot, {0x8b,0x8b,0xa0,0x84,0x02,0x00}}, // mov ecx, dword ptr [rbx + 0x284a0] +{0x3e6a852, 7, 3, Value::slot, {0x48,0x8b,0x93,0xb8,0x84,0x02,0x00}}, // mov rdx, qword ptr [rbx + 0x284b8] +{0x3e6a868, 7, 3, Value::slot, {0x48,0x8b,0x8b,0xa8,0x84,0x02,0x00}}, // mov rcx, qword ptr [rbx + 0x284a8] +{0x3e6a874, 7, 3, Value::slot, {0x4c,0x89,0xa3,0xa8,0x84,0x02,0x00}}, // mov qword ptr [rbx + 0x284a8], r12 +{0x3e6a881, 7, 3, Value::slot, {0x4c,0x89,0xa3,0xb8,0x84,0x02,0x00}}, // mov qword ptr [rbx + 0x284b8], r12 +{0x3e6c682, 7, 3, Value::slot, {0x48,0x81,0xc1,0x98,0x84,0x02,0x00}}, // add rcx, 0x28498 +{0x3e6c8fd, 7, 3, Value::floats, {0x48,0x8d,0x81,0x00,0x05,0x1e,0x00}}, // lea rax, [rcx + 0x1e0500] +{0x3e6c904, 7, 3, Value::count, {0xc7,0x42,0x08,0x00,0x50,0x00,0x00}}, // mov dword ptr [rdx + 8], 0x5000 +{0x3e6d597, 8, 3, Value::slot, {0x80,0xbc,0x0a,0x9a,0x84,0x02,0x00,0x00}}, // cmp byte ptr [rdx + rcx + 0x2849a], 0 +{0x3e6d945, 7, 3, Value::slot, {0x48,0x8d,0xbe,0xc0,0x84,0x02,0x00}}, // lea rdi, [rsi + 0x284c0] +{0x3e6d98b, 7, 3, Value::slot, {0x48,0x8d,0x96,0xdc,0x84,0x02,0x00}}, // lea rdx, [rsi + 0x284dc] +{0x3e6d99d, 9, 5, Value::slot, {0x45,0x0f,0xb6,0x8c,0x34,0x99,0x84,0x02,0x00}}, // movzx r9d, byte ptr [r12 + rsi + 0x28499] +{0x3e6d9ab, 8, 4, Value::slot, {0x45,0x2a,0x8c,0x34,0x9b,0x84,0x02,0x00}}, // sub r9b, byte ptr [r12 + rsi + 0x2849b] +{0x3e6d9cd, 8, 4, Value::slot, {0x4d,0x8b,0x8c,0x34,0xb8,0x84,0x02,0x00}}, // mov r9, qword ptr [r12 + rsi + 0x284b8] +{0x3e6d9df, 7, 3, Value::slot, {0x48,0x8d,0xbe,0xc0,0x84,0x02,0x00}}, // lea rdi, [rsi + 0x284c0] +{0x3e6d9ee, 8, 4, Value::slot, {0x41,0x89,0x84,0x34,0xe0,0x84,0x02,0x00}}, // mov dword ptr [r12 + rsi + 0x284e0], eax +{0x3e6dac5, 9, 5, Value::slot, {0x45,0x0f,0xb6,0x84,0x36,0x99,0x84,0x02,0x00}}, // movzx r8d, byte ptr [r14 + rsi + 0x28499] +{0x3e6dad1, 8, 4, Value::slot, {0x45,0x2a,0x84,0x36,0x9b,0x84,0x02,0x00}}, // sub r8b, byte ptr [r14 + rsi + 0x2849b] +{0x3e6daeb, 8, 4, Value::slot, {0x44,0x38,0xac,0x30,0xa7,0x84,0x02,0x00}}, // cmp byte ptr [rax + rsi + 0x284a7], r13b +{0x3e6db01, 8, 4, Value::slot, {0x49,0x8b,0xbc,0x36,0xd0,0x84,0x02,0x00}}, // mov rdi, qword ptr [r14 + rsi + 0x284d0] +{0x3e6db2d, 9, 4, Value::slot, {0x41,0x80,0xa4,0x36,0xa7,0x84,0x02,0x00,0xfd}}, // and byte ptr [r14 + rsi + 0x284a7], 0xfd +{0x3e6e54f, 8, 4, Value::slot, {0x0f,0xb6,0x84,0x01,0xa7,0x84,0x02,0x00}}, // movzx eax, byte ptr [rcx + rax + 0x284a7] +{0x3e6e587, 9, 5, Value::slot, {0x46,0x0f,0xb6,0x84,0x12,0x9c,0x84,0x02,0x00}}, // movzx r8d, byte ptr [rdx + r10 + 0x2849c] +{0x3e6e590, 9, 5, Value::slot, {0x42,0x0f,0xb6,0x8c,0x12,0x9e,0x84,0x02,0x00}}, // movzx ecx, byte ptr [rdx + r10 + 0x2849e] +{0x3e6e6ac, 9, 5, Value::slot, {0x41,0x0f,0xb6,0x84,0x09,0xa5,0x84,0x02,0x00}}, // movzx eax, byte ptr [r9 + rcx + 0x284a5] +{0x3e6e6c0, 9, 5, Value::slot, {0x41,0x0f,0xb6,0x84,0x09,0xa4,0x84,0x02,0x00}}, // movzx eax, byte ptr [r9 + rcx + 0x284a4] +{0x3e6e6ce, 9, 5, Value::slot, {0x41,0x0f,0xb6,0x84,0x09,0xa4,0x84,0x02,0x00}}, // movzx eax, byte ptr [r9 + rcx + 0x284a4] +{0x3e6e6d9, 9, 5, Value::slot, {0x41,0x0f,0xb6,0x9c,0x09,0x99,0x84,0x02,0x00}}, // movzx ebx, byte ptr [r9 + rcx + 0x28499] +{0x3e6e6e2, 9, 5, Value::slot, {0x41,0x0f,0xb6,0xb4,0x09,0x9c,0x84,0x02,0x00}}, // movzx esi, byte ptr [r9 + rcx + 0x2849c] +{0x3e6e7a7, 9, 4, Value::slot, {0x48,0x83,0xbc,0x0a,0xa8,0x84,0x02,0x00,0x00}}, // cmp qword ptr [rdx + rcx + 0x284a8], 0 +{0x3e6e859, 7, 3, Value::slot, {0x48,0x81,0xc3,0x98,0x84,0x02,0x00}}, // add rbx, 0x28498 +{0x3e6ed5f, 8, 4, Value::slot, {0x48,0x89,0x94,0x01,0xb8,0x84,0x02,0x00}}, // mov qword ptr [rcx + rax + 0x284b8], rdx +{0x3e722a1, 8, 4, Value::slot, {0x4a,0x8b,0x84,0x26,0xb8,0x84,0x02,0x00}}, // mov rax, qword ptr [rsi + r12 + 0x284b8] +{0x3e72314, 8, 4, Value::slot, {0x4a,0x8b,0x84,0x26,0xb8,0x84,0x02,0x00}}, // mov rax, qword ptr [rsi + r12 + 0x284b8] +{0x3e72351, 8, 4, Value::slot, {0x42,0xfe,0x84,0x26,0xa6,0x84,0x02,0x00}}, // inc byte ptr [rsi + r12 + 0x284a6] +{0x3e72359, 8, 4, Value::slot, {0x4e,0x89,0xbc,0x26,0xc8,0x84,0x02,0x00}}, // mov qword ptr [rsi + r12 + 0x284c8], r15 +{0x3e72369, 8, 4, Value::slot, {0x42,0x2b,0x84,0x26,0xa0,0x84,0x02,0x00}}, // sub eax, dword ptr [rsi + r12 + 0x284a0] +{0x3e72b93, 7, 3, Value::slot, {0x48,0x81,0xc6,0x98,0x84,0x02,0x00}}, // add rsi, 0x28498 +{0x3e72bd6, 6, 2, Value::count, {0x41,0xbc,0x00,0x50,0x00,0x00}}, // mov r12d, 0x5000 +{0x3e72bfe, 8, 3, Value::slot, {0x48,0x83,0xbe,0xa8,0x84,0x02,0x00,0x00}}, // cmp qword ptr [rsi + 0x284a8], 0 +{0x3e72c20, 7, 3, Value::slot, {0x48,0x81,0xc6,0x98,0x84,0x02,0x00}}, // add rsi, 0x28498 +{0x3e72c7a, 5, 1, Value::count, {0xb9,0x00,0x50,0x00,0x00}}, // mov ecx, 0x5000 +{0x3e72ca3, 6, 2, Value::count, {0x81,0xc1,0x00,0x50,0x00,0x00}}, // add ecx, 0x5000 +{0x3e72d24, 7, 3, Value::slot, {0x48,0x81,0xc6,0x98,0x84,0x02,0x00}}, // add rsi, 0x28498 +{0x3e738df, 9, 4, Value::slot, {0x43,0x80,0xbc,0x37,0x9c,0x84,0x02,0x00,0x00}}, // cmp byte ptr [r15 + r14 + 0x2849c], 0 +{0x3e738e8, 9, 5, Value::slot, {0x66,0x43,0x89,0x9c,0x37,0xe6,0x84,0x02,0x00}}, // mov word ptr [r15 + r14 + 0x284e6], bx +{0x3e738f7, 9, 4, Value::slot, {0x43,0x80,0xbc,0x37,0xa6,0x84,0x02,0x00,0x00}}, // cmp byte ptr [r15 + r14 + 0x284a6], 0 +{0x3e73906, 9, 4, Value::slot, {0x43,0xf6,0x84,0x37,0xa7,0x84,0x02,0x00,0x02}}, // test byte ptr [r15 + r14 + 0x284a7], 2 +{0x3e7391f, 9, 5, Value::slot, {0x66,0x43,0x89,0x84,0x37,0xe4,0x84,0x02,0x00}}, // mov word ptr [r15 + r14 + 0x284e4], ax +{0x3e7394a, 9, 5, Value::slot, {0x43,0x0f,0xb6,0xbc,0x37,0x99,0x84,0x02,0x00}}, // movzx edi, byte ptr [r15 + r14 + 0x28499] +{0x3e73953, 9, 5, Value::slot, {0x66,0x43,0x89,0x84,0x37,0xe8,0x84,0x02,0x00}}, // mov word ptr [r15 + r14 + 0x284e8], ax +{0x3e7395c, 8, 4, Value::slot, {0x47,0x88,0xa4,0x37,0x9b,0x84,0x02,0x00}}, // mov byte ptr [r15 + r14 + 0x2849b], r12b +{0x3e73977, 8, 4, Value::slot, {0x47,0x88,0xa4,0x37,0x9a,0x84,0x02,0x00}}, // mov byte ptr [r15 + r14 + 0x2849a], r12b +{0x3e7399e, 8, 4, Value::slot, {0x43,0x88,0x94,0x37,0xa4,0x84,0x02,0x00}}, // mov byte ptr [r15 + r14 + 0x284a4], dl +{0x3e739c7, 8, 4, Value::slot, {0x47,0x88,0x84,0x37,0xa5,0x84,0x02,0x00}}, // mov byte ptr [r15 + r14 + 0x284a5], r8b +{0x3e739e5, 8, 4, Value::slot, {0x4b,0x8b,0x8c,0x37,0xa8,0x84,0x02,0x00}}, // mov rcx, qword ptr [r15 + r14 + 0x284a8] +{0x3e73a00, 7, 3, Value::slot, {0x49,0x8d,0x96,0xdc,0x84,0x02,0x00}}, // lea rdx, [r14 + 0x284dc] +{0x3e73a2a, 8, 4, Value::slot, {0x4f,0x8b,0x8c,0x37,0xa8,0x84,0x02,0x00}}, // mov r9, qword ptr [r15 + r14 + 0x284a8] +{0x3e73ab9, 9, 5, Value::slot, {0x43,0x0f,0xb6,0x84,0x37,0xa7,0x84,0x02,0x00}}, // movzx eax, byte ptr [r15 + r14 + 0x284a7] +{0x3e73acc, 8, 4, Value::slot, {0x43,0x88,0x8c,0x37,0xa7,0x84,0x02,0x00}}, // mov byte ptr [r15 + r14 + 0x284a7], cl +{0x3e74db3, 8, 4, Value::slot, {0x0f,0xb7,0x8c,0x31,0xe8,0x84,0x02,0x00}}, // movzx ecx, word ptr [rcx + rsi + 0x284e8] +{0x3e74f4d, 8, 4, Value::slot, {0x4a,0x8b,0x84,0x0e,0xc0,0x84,0x02,0x00}}, // mov rax, qword ptr [rsi + r9 + 0x284c0] +{0x3e74f5a, 8, 4, Value::slot, {0x4d,0x89,0xa4,0x31,0xc0,0x84,0x02,0x00}}, // mov qword ptr [r9 + rsi + 0x284c0], r12 +{0x3e74f6a, 8, 4, Value::slot, {0x4d,0x8b,0x8c,0x31,0xb8,0x84,0x02,0x00}}, // mov r9, qword ptr [r9 + rsi + 0x284b8] +{0x3e75143, 8, 4, Value::slot, {0x0f,0xb7,0x8c,0x31,0xe8,0x84,0x02,0x00}}, // movzx ecx, word ptr [rcx + rsi + 0x284e8] +{0x3e75273, 9, 5, Value::slot, {0x44,0x0f,0xb6,0x8c,0x33,0x9c,0x84,0x02,0x00}}, // movzx r9d, byte ptr [rbx + rsi + 0x2849c] +{0x3e752b7, 8, 4, Value::slot, {0x48,0x8b,0x8c,0x33,0xb8,0x84,0x02,0x00}}, // mov rcx, qword ptr [rbx + rsi + 0x284b8] +{0x3e752c3, 8, 4, Value::slot, {0x4c,0x89,0xbc,0x33,0xb8,0x84,0x02,0x00}}, // mov qword ptr [rbx + rsi + 0x284b8], r15 +{0x3e7572f, 7, 3, Value::slot, {0x49,0x8d,0x9e,0x98,0x84,0x02,0x00}}, // lea rbx, [r14 + 0x28498] +{0x3e7580a, 10, 6, Value::floats, {0xc4,0xc1,0x4a,0x5e,0x84,0x8e,0x00,0x05,0x1e,0x00}}, // vdivss xmm0, xmm6, dword ptr [r14 + rcx*4 + 0x1e0500] +{0x3e75b5d, 9, 4, Value::slot, {0x42,0xf6,0x84,0x31,0xa7,0x84,0x02,0x00,0x08}}, // test byte ptr [rcx + r14 + 0x284a7], 8 +{0x3e75b7f, 9, 4, Value::slot, {0x42,0xf6,0x84,0x31,0xa7,0x84,0x02,0x00,0x08}}, // test byte ptr [rcx + r14 + 0x284a7], 8 +{0x3e75bae, 9, 5, Value::slot, {0x42,0x0f,0xb6,0x94,0x31,0xa7,0x84,0x02,0x00}}, // movzx edx, byte ptr [rcx + r14 + 0x284a7] +{0x3e75bb7, 9, 5, Value::slot, {0x46,0x0f,0xb6,0x84,0x31,0x9e,0x84,0x02,0x00}}, // movzx r8d, byte ptr [rcx + r14 + 0x2849e] +{0x3e75bca, 9, 5, Value::slot, {0x42,0x0f,0xb6,0x84,0x31,0x9c,0x84,0x02,0x00}}, // movzx eax, byte ptr [rcx + r14 + 0x2849c] +{0x3e75c04, 9, 4, Value::slot, {0x43,0x80,0xbc,0x34,0xa7,0x84,0x02,0x00,0x00}}, // cmp byte ptr [r12 + r14 + 0x284a7], 0 +{0x3e75c13, 9, 5, Value::slot, {0x66,0x47,0x39,0x84,0x34,0xe8,0x84,0x02,0x00}}, // cmp word ptr [r12 + r14 + 0x284e8], r8w +{0x3e75c52, 9, 5, Value::slot, {0x42,0x0f,0xb7,0x8c,0x31,0xe8,0x84,0x02,0x00}}, // movzx ecx, word ptr [rcx + r14 + 0x284e8] +{0x3e75cb9, 8, 4, Value::slot, {0x49,0x8b,0x8c,0x36,0xa8,0x84,0x02,0x00}}, // mov rcx, qword ptr [r14 + rsi + 0x284a8] +{0x3e75cca, 7, 3, Value::slot, {0x49,0x8d,0x96,0xdc,0x84,0x02,0x00}}, // lea rdx, [r14 + 0x284dc] +{0x3e75cf3, 8, 4, Value::slot, {0x4d,0x8b,0x8c,0x36,0xa8,0x84,0x02,0x00}}, // mov r9, qword ptr [r14 + rsi + 0x284a8] +{0x3e75d4e, 8, 4, Value::slot, {0x4b,0x8b,0x8c,0x34,0xa8,0x84,0x02,0x00}}, // mov rcx, qword ptr [r12 + r14 + 0x284a8] +{0x3e75d5f, 7, 3, Value::slot, {0x49,0x8d,0x96,0xdc,0x84,0x02,0x00}}, // lea rdx, [r14 + 0x284dc] +{0x3e75d87, 8, 4, Value::slot, {0x4f,0x8b,0x8c,0x34,0xa8,0x84,0x02,0x00}}, // mov r9, qword ptr [r12 + r14 + 0x284a8] +{0x3e75dc3, 9, 4, Value::slot, {0x42,0x80,0xbc,0x31,0xa7,0x84,0x02,0x00,0x00}}, // cmp byte ptr [rcx + r14 + 0x284a7], 0 +{0x3e75dce, 9, 5, Value::slot, {0x66,0x46,0x39,0x84,0x31,0xe8,0x84,0x02,0x00}}, // cmp word ptr [rcx + r14 + 0x284e8], r8w +{0x3e75e03, 9, 5, Value::slot, {0x42,0x0f,0xb7,0x8c,0x31,0xe8,0x84,0x02,0x00}}, // movzx ecx, word ptr [rcx + r14 + 0x284e8] +{0x3e75e55, 8, 4, Value::slot, {0x4a,0x8b,0x8c,0x33,0xa8,0x84,0x02,0x00}}, // mov rcx, qword ptr [rbx + r14 + 0x284a8] +{0x3e75e6d, 7, 3, Value::slot, {0x48,0x8d,0x97,0xdc,0x84,0x02,0x00}}, // lea rdx, [rdi + 0x284dc] +{0x3e75e80, 9, 5, Value::slot, {0x42,0x0f,0xb6,0x8c,0x33,0x9b,0x84,0x02,0x00}}, // movzx ecx, byte ptr [rbx + r14 + 0x2849b] +{0x3e75e89, 9, 5, Value::slot, {0x42,0x0f,0xb6,0x94,0x33,0x99,0x84,0x02,0x00}}, // movzx edx, byte ptr [rbx + r14 + 0x28499] +{0x3e75e94, 9, 5, Value::slot, {0x42,0x0f,0xb6,0x8c,0x33,0x9c,0x84,0x02,0x00}}, // movzx ecx, byte ptr [rbx + r14 + 0x2849c] +{0x3e75ea5, 9, 4, Value::slot, {0x42,0x80,0xbc,0x33,0xa6,0x84,0x02,0x00,0x00}}, // cmp byte ptr [rbx + r14 + 0x284a6], 0 +{0x3e75eb4, 9, 4, Value::slot, {0x42,0xf6,0x84,0x33,0xa7,0x84,0x02,0x00,0x08}}, // test byte ptr [rbx + r14 + 0x284a7], 8 +{0x3e75ec3, 8, 4, Value::slot, {0x4a,0x8b,0x94,0x33,0xb8,0x84,0x02,0x00}}, // mov rdx, qword ptr [rbx + r14 + 0x284b8] +{0x3e75ee2, 7, 3, Value::slot, {0x48,0x8d,0x97,0xdc,0x84,0x02,0x00}}, // lea rdx, [rdi + 0x284dc] +{0x3e75ef6, 9, 5, Value::slot, {0x46,0x0f,0xb6,0x84,0x33,0x9c,0x84,0x02,0x00}}, // movzx r8d, byte ptr [rbx + r14 + 0x2849c] +{0x3e75f0f, 9, 5, Value::slot, {0x42,0x0f,0xb6,0x94,0x33,0x9e,0x84,0x02,0x00}}, // movzx edx, byte ptr [rbx + r14 + 0x2849e] +{0x3e75f1d, 9, 5, Value::slot, {0x42,0x0f,0xb6,0x8c,0x33,0x9c,0x84,0x02,0x00}}, // movzx ecx, byte ptr [rbx + r14 + 0x2849c] +{0x3e75f4f, 9, 4, Value::slot, {0x42,0x80,0xbc,0x37,0xa7,0x84,0x02,0x00,0x00}}, // cmp byte ptr [rdi + r14 + 0x284a7], 0 +{0x3e75f7f, 7, 3, Value::slot, {0x49,0x8d,0x96,0xdc,0x84,0x02,0x00}}, // lea rdx, [r14 + 0x284dc] +{0x3e75f95, 8, 4, Value::slot, {0x4a,0x8b,0x8c,0x37,0xa8,0x84,0x02,0x00}}, // mov rcx, qword ptr [rdi + r14 + 0x284a8] +{0x3e75fb1, 8, 4, Value::slot, {0x4a,0x8b,0x94,0x37,0xb8,0x84,0x02,0x00}}, // mov rdx, qword ptr [rdi + r14 + 0x284b8] +{0x3e75fbc, 8, 4, Value::slot, {0x42,0x8b,0x8c,0x37,0xa0,0x84,0x02,0x00}}, // mov ecx, dword ptr [rdi + r14 + 0x284a0] +{0x3e75fc4, 9, 5, Value::slot, {0x42,0x0f,0xb6,0x84,0x37,0x99,0x84,0x02,0x00}}, // movzx eax, byte ptr [rdi + r14 + 0x28499] +{0x3e75fcf, 8, 4, Value::slot, {0x42,0x88,0x84,0x37,0x9d,0x84,0x02,0x00}}, // mov byte ptr [rdi + r14 + 0x2849d], al +{0x3e75fd7, 8, 4, Value::slot, {0x42,0x88,0x84,0x37,0x9b,0x84,0x02,0x00}}, // mov byte ptr [rdi + r14 + 0x2849b], al +{0x3e75fdf, 12, 4, Value::slot, {0x4a,0xc7,0x84,0x37,0xb8,0x84,0x02,0x00,0x00,0x00,0x00,0x00}}, // mov qword ptr [rdi + r14 + 0x284b8], 0 +{0x3e75feb, 12, 4, Value::slot, {0x42,0xc7,0x84,0x37,0xa0,0x84,0x02,0x00,0x00,0x00,0x00,0x00}}, // mov dword ptr [rdi + r14 + 0x284a0], 0 +{0x3e75ff7, 9, 4, Value::slot, {0x42,0xc6,0x84,0x37,0x9c,0x84,0x02,0x00,0x00}}, // mov byte ptr [rdi + r14 + 0x2849c], 0 +{0x3e760bd, 9, 4, Value::slot, {0x42,0xf6,0x84,0x31,0xa7,0x84,0x02,0x00,0x08}}, // test byte ptr [rcx + r14 + 0x284a7], 8 +{0x3e76113, 9, 4, Value::slot, {0x42,0xf6,0x84,0x31,0xa7,0x84,0x02,0x00,0x08}}, // test byte ptr [rcx + r14 + 0x284a7], 8 +{0x3e7612e, 8, 4, Value::slot, {0x46,0x38,0x84,0x31,0x9c,0x84,0x02,0x00}}, // cmp byte ptr [rcx + r14 + 0x2849c], r8b +{0x3e76155, 9, 4, Value::slot, {0x42,0x80,0xbc,0x30,0xa7,0x84,0x02,0x00,0x00}}, // cmp byte ptr [rax + r14 + 0x284a7], 0 +{0x3e76203, 7, 3, Value::slot, {0x48,0x8d,0x93,0xdc,0x84,0x02,0x00}}, // lea rdx, [rbx + 0x284dc] +{0x3e76212, 7, 3, Value::slot, {0x0f,0xb6,0x83,0x9c,0x84,0x02,0x00}}, // movzx eax, byte ptr [rbx + 0x2849c] +{0x3e76223, 7, 2, Value::slot, {0x80,0xbb,0xa7,0x84,0x02,0x00,0x00}}, // cmp byte ptr [rbx + 0x284a7], 0 +{0x3e7623b, 7, 3, Value::slot, {0x48,0x8b,0xb3,0xc0,0x84,0x02,0x00}}, // mov rsi, qword ptr [rbx + 0x284c0] +{0x3e76251, 6, 2, Value::slot, {0x8b,0x83,0xe0,0x84,0x02,0x00}}, // mov eax, dword ptr [rbx + 0x284e0] +{0x3e7625d, 7, 3, Value::slot, {0x48,0x89,0xbb,0xc0,0x84,0x02,0x00}}, // mov qword ptr [rbx + 0x284c0], rdi +{0x3e76264, 7, 3, Value::slot, {0x4c,0x8d,0xab,0xb8,0x84,0x02,0x00}}, // lea r13, [rbx + 0x284b8] +{0x3e76279, 7, 3, Value::slot, {0x48,0x89,0xbb,0xc0,0x84,0x02,0x00}}, // mov qword ptr [rbx + 0x284c0], rdi +{0x3e76283, 7, 3, Value::slot, {0x4c,0x8d,0xab,0xb8,0x84,0x02,0x00}}, // lea r13, [rbx + 0x284b8] +{0x3e76311, 7, 3, Value::slot, {0x0f,0xb6,0x83,0x9c,0x84,0x02,0x00}}, // movzx eax, byte ptr [rbx + 0x2849c] +{0x3e76321, 7, 3, Value::slot, {0x0f,0xb6,0x83,0x99,0x84,0x02,0x00}}, // movzx eax, byte ptr [rbx + 0x28499] +{0x3e7632d, 7, 3, Value::slot, {0x0f,0xb6,0x83,0xa7,0x84,0x02,0x00}}, // movzx eax, byte ptr [rbx + 0x284a7] +{0x3e7633c, 7, 3, Value::slot, {0x44,0x8b,0x83,0xd8,0x84,0x02,0x00}}, // mov r8d, dword ptr [rbx + 0x284d8] +{0x3e763bc, 6, 2, Value::slot, {0xfe,0x83,0xa6,0x84,0x02,0x00}}, // inc byte ptr [rbx + 0x284a6] +{0x3e763ca, 7, 3, Value::slot, {0x48,0x89,0xb3,0xc8,0x84,0x02,0x00}}, // mov qword ptr [rbx + 0x284c8], rsi +{0x3e763da, 6, 2, Value::slot, {0x2b,0x83,0xa0,0x84,0x02,0x00}}, // sub eax, dword ptr [rbx + 0x284a0] +{0x3e76a49, 7, 3, Value::slot, {0x48,0x81,0xc3,0x98,0x84,0x02,0x00}}, // add rbx, 0x28498 +{0x3e77038, 10, 5, Value::slot, {0x66,0x41,0x83,0xbc,0x06,0xea,0x84,0x02,0x00,0x01}}, // cmp word ptr [r14 + rax + 0x284ea], 1 +{0x3e77044, 9, 4, Value::slot, {0x42,0x80,0xbc,0x30,0xa6,0x84,0x02,0x00,0x00}}, // cmp byte ptr [rax + r14 + 0x284a6], 0 +{0x3e7704f, 8, 4, Value::slot, {0x4a,0x8b,0x84,0x30,0xb0,0x84,0x02,0x00}}, // mov rax, qword ptr [rax + r14 + 0x284b0] +{0x3e7708c, 7, 3, Value::slot, {0x49,0x8d,0x8e,0x98,0x84,0x02,0x00}}, // lea rcx, [r14 + 0x28498] +{0x3e77518, 8, 4, Value::slot, {0x66,0x89,0x84,0x29,0xe6,0x84,0x02,0x00}}, // mov word ptr [rcx + rbp + 0x284e6], ax +{0x3e77af3, 9, 5, Value::floats, {0xc5,0xfa,0x10,0x8c,0x86,0x00,0x05,0x1e,0x00}}, // vmovss xmm1, dword ptr [rsi + rax*4 + 0x1e0500] +{0x3e77b16, 9, 5, Value::floats, {0xc5,0xfa,0x11,0x84,0x86,0x00,0x05,0x1e,0x00}}, // vmovss dword ptr [rsi + rax*4 + 0x1e0500], xmm0 +{0x3e77ec4, 7, 3, Value::floats, {0x48,0x8d,0x86,0x00,0x05,0x1e,0x00}}, // lea rax, [rsi + 0x1e0500] +{0x3e77ed0, 7, 3, Value::float_bytes, {0x48,0x8d,0x88,0x00,0x40,0x01,0x00}}, // lea rcx, [rax + 0x14000] +{0x3efd80a, 5, 1, Value::alloc, {0xba,0x80,0xe7,0x24,0x00}}, // mov edx, 0x24e780 +{0x3f31cc8, 7, 3, Value::r_field, {0x48,0x8b,0x86,0x80,0x00,0x05,0x00}}, // mov rax, qword ptr [rsi + 0x50080] +{0x3f31d22, 7, 3, Value::r_field, {0x48,0x8b,0x86,0x80,0x00,0x05,0x00}}, // mov rax, qword ptr [rsi + 0x50080] +{0x3f34a5b, 7, 3, Value::r_field, {0x48,0x8b,0x88,0x80,0x00,0x05,0x00}}, // mov rcx, qword ptr [rax + 0x50080] +{0x3f34a8b, 7, 3, Value::r_field, {0x48,0x8b,0x90,0x80,0x00,0x05,0x00}}, // mov rdx, qword ptr [rax + 0x50080] +{0x3f34e69, 7, 3, Value::r_field, {0x48,0x8b,0xa8,0x80,0x00,0x05,0x00}}, // mov rbp, qword ptr [rax + 0x50080] +{0x3f34ef3, 7, 3, Value::r_field, {0x48,0x8b,0x89,0x80,0x00,0x05,0x00}}, // mov rcx, qword ptr [rcx + 0x50080] +{0x3f3ed33, 7, 3, Value::r_field, {0x48,0x8b,0x88,0x80,0x00,0x05,0x00}}, // mov rcx, qword ptr [rax + 0x50080] +{0x3f3d83a, 5, 1, Value::r_alloc, {0xb9,0xc0,0x00,0x05,0x00}}, // mov ecx, 0x500c0 +{0x3f3d86b, 5, 1, Value::r_fill, {0xb9,0x00,0x40,0x01,0x00}}, // mov ecx, 0x14000 +{0x3f3d876, 7, 3, Value::r_block, {0x49,0x8d,0x80,0x40,0x40,0x01,0x00}}, // lea rax, [r8 + 0x14040] +{0x3f3d87d, 7, 3, Value::r_field, {0x49,0x89,0x90,0x40,0x00,0x05,0x00}}, // mov qword ptr [r8 + 0x50040], rdx +{0x3f3d884, 7, 3, Value::r_block, {0x49,0x8d,0x90,0x40,0x80,0x02,0x00}}, // lea rdx, [r8 + 0x28040] +{0x3f3d88b, 7, 3, Value::r_field, {0x41,0x89,0x88,0x4c,0x00,0x05,0x00}}, // mov dword ptr [r8 + 0x5004c], ecx +{0x3f3d892, 11, 3, Value::r_field, {0x41,0xc7,0x80,0x48,0x00,0x05,0x00,0x00,0x50,0x00,0x00}}, // mov dword ptr [r8 + 0x50048], 0x5000 +{0x3f3d892, 11, 7, Value::count, {0x41,0xc7,0x80,0x48,0x00,0x05,0x00,0x00,0x50,0x00,0x00}}, // mov dword ptr [r8 + 0x50048], 0x5000 +{0x3f3d89d, 7, 3, Value::r_field, {0x49,0x89,0x80,0x50,0x00,0x05,0x00}}, // mov qword ptr [r8 + 0x50050], rax +{0x3f3d8a7, 11, 3, Value::r_field, {0x41,0xc7,0x80,0x58,0x00,0x05,0x00,0x00,0x50,0x00,0x00}}, // mov dword ptr [r8 + 0x50058], 0x5000 +{0x3f3d8a7, 11, 7, Value::count, {0x41,0xc7,0x80,0x58,0x00,0x05,0x00,0x00,0x50,0x00,0x00}}, // mov dword ptr [r8 + 0x50058], 0x5000 +{0x3f3d8b2, 7, 3, Value::r_field, {0x41,0x89,0x88,0x5c,0x00,0x05,0x00}}, // mov dword ptr [r8 + 0x5005c], ecx +{0x3f3d8b9, 7, 3, Value::r_field, {0x49,0x89,0x90,0x60,0x00,0x05,0x00}}, // mov qword ptr [r8 + 0x50060], rdx +{0x3f3d8c0, 7, 3, Value::r_block, {0x49,0x8d,0x90,0x40,0xc0,0x03,0x00}}, // lea rdx, [r8 + 0x3c040] +{0x3f3d8c7, 11, 3, Value::r_field, {0x41,0xc7,0x80,0x68,0x00,0x05,0x00,0x00,0x50,0x00,0x00}}, // mov dword ptr [r8 + 0x50068], 0x5000 +{0x3f3d8c7, 11, 7, Value::count, {0x41,0xc7,0x80,0x68,0x00,0x05,0x00,0x00,0x50,0x00,0x00}}, // mov dword ptr [r8 + 0x50068], 0x5000 +{0x3f3d8d2, 7, 3, Value::r_field, {0x41,0x89,0x88,0x6c,0x00,0x05,0x00}}, // mov dword ptr [r8 + 0x5006c], ecx +{0x3f3d8d9, 7, 3, Value::r_field, {0x49,0x89,0x90,0x70,0x00,0x05,0x00}}, // mov qword ptr [r8 + 0x50070], rdx +{0x3f3d8e0, 11, 3, Value::r_field, {0x41,0xc7,0x80,0x78,0x00,0x05,0x00,0x00,0x50,0x00,0x00}}, // mov dword ptr [r8 + 0x50078], 0x5000 +{0x3f3d8e0, 11, 7, Value::count, {0x41,0xc7,0x80,0x78,0x00,0x05,0x00,0x00,0x50,0x00,0x00}}, // mov dword ptr [r8 + 0x50078], 0x5000 +{0x3f3d8eb, 7, 3, Value::r_field, {0x41,0x89,0x88,0x7c,0x00,0x05,0x00}}, // mov dword ptr [r8 + 0x5007c], ecx +{0x3f3d8f2, 7, 3, Value::r_field, {0x49,0x89,0x98,0x80,0x00,0x05,0x00}}, // mov qword ptr [r8 + 0x50080], rbx +{0x3f3d909, 7, 3, Value::r_field, {0x48,0x89,0x9e,0x80,0x00,0x05,0x00}}, // mov qword ptr [rsi + 0x50080], rbx +{0x3f56580, 7, 3, Value::r_spans, {0x48,0x8d,0x98,0x04,0x50,0x00,0x00}}, // lea rbx, [rax + 0x5004] diff --git a/Extension/World/stack_check_report.cpp b/Extension/World/stack_check_report.cpp new file mode 100644 index 0000000..a51a63b --- /dev/null +++ b/Extension/World/stack_check_report.cpp @@ -0,0 +1,62 @@ +#include "stack_check_report.h" +#include "Engine/Core/Hooks/hooks.h" +#include "Engine/Core/Log/logging.h" +#include "Engine/Core/Platform/memory.h" +#include "Engine/Game/Build/addresses.h" +#include "Engine/Game/Build/20260929/texture_streaming.h" +#include +#include +#include +#include +#include + +namespace dingosdk::stack_check_report { +namespace { +namespace ts = addr::texture_streaming; +using Report = void (*)(std::uintptr_t cookie); +Report original{}; +std::uintptr_t image{}; +std::atomic installed{}; + +std::string place(std::uintptr_t address) { + char text[40]{}; + if (address >= image && address - image < 0x10000000) sprintf_s(text, "Skate.exe+0x%llx", static_cast(address - image)); + else sprintf_s(text, "0x%llx", static_cast(address)); + return text; +} +// The game is about to end itself with a fast fail, which no crash handler sees: say where +// first. The return address is in the function whose stack was written over; the frames behind +// it may be part of what was written over. +void reported(std::uintptr_t cookie) { + const auto from = reinterpret_cast(_ReturnAddress()); + try { + void* frames[24]{}; + const auto count = CaptureStackBackTrace(1, 24, frames, nullptr); + std::string trace; + for (unsigned i = 0; i < count; ++i) trace += (i ? " < " : "") + place(reinterpret_cast(frames[i])); + logging::log(logging::Level::error, logging::Channel::runtime, + "The game's stack check failed in the function that returns to {} (it ends the game at once, with no crash report). Callers: {}", + place(from), trace.empty() ? std::string("unknown") : trace); + logging::flush(); + } catch (...) {} + original(cookie); +} +} // namespace + +bool install(std::uintptr_t base) noexcept { + if (installed.exchange(true)) return original != nullptr; + if (!base) return false; + image = base; + std::array head{}; + if (!memory::peek(base + ts::stack_check_failed, head) || head != ts::stack_check_failed_bytes) return false; + const auto target = reinterpret_cast(base + ts::stack_check_failed); + if (hook_prepare(target, reinterpret_cast(&reported), reinterpret_cast(&original)) != HookOk || hook_enable(target) != HookOk) { + original = nullptr; + try { + logging::log(logging::Level::warning, logging::Channel::runtime, "Stack check report: the game's routine could not be hooked."); + } catch (...) {} + return false; + } + return true; +} +} // namespace dingosdk::stack_check_report diff --git a/Extension/World/stack_check_report.h b/Extension/World/stack_check_report.h new file mode 100644 index 0000000..17abf08 --- /dev/null +++ b/Extension/World/stack_check_report.h @@ -0,0 +1,10 @@ +#pragma once +#include + +// When a function of the game finds its stack written over, the game ends itself with a fast +// fail (exit code 0xc0000409): no exception, so neither crash reporter writes anything. This +// puts where it happened in the log first. A diagnostic for work on the game's fixed-size +// tables (texture_handle_limit); it changes nothing else. +namespace dingosdk::stack_check_report { +bool install(std::uintptr_t base) noexcept; +} diff --git a/Extension/World/texture_handle_limit.cpp b/Extension/World/texture_handle_limit.cpp new file mode 100644 index 0000000..edb9650 --- /dev/null +++ b/Extension/World/texture_handle_limit.cpp @@ -0,0 +1,262 @@ +#include "texture_handle_limit.h" +#include "Engine/Core/Hooks/hooks.h" +#include "Engine/Core/Log/logging.h" +#include "Engine/Core/Platform/memory.h" +#include "Engine/Game/Build/addresses.h" +#include "Engine/Game/Build/20260929/texture_streaming.h" +#include "Extension/UI/Overlay/overlay.h" +#include +#include +#include +#include +#include +#include + +namespace dingosdk::texture_handle_limit { +namespace { +namespace ts = addr::texture_streaming; +using Constructor = void* (*)(void* manager, void* arena); +Constructor original{}; +std::uintptr_t image{}; +std::atomic installed{}, raised{}; +std::atomic lists_moved{}; + +std::uint32_t stock_value(const ts::Site& site) noexcept { + std::uint32_t value{}; + std::memcpy(&value, site.bytes + site.at, 4); + return value; +} +// Each list's entries go behind the manager's stock end, with what the constructor put in them. +// A list that is not where the constructor leaves it is left alone. +bool move_lists(std::uintptr_t manager) noexcept { + __try { + unsigned moved{}; + for (const auto& list : ts::lists) { + auto* header = reinterpret_cast(manager + list.header); + const auto stock = manager + list.stock_entries; + if (header[0] != stock || header[1] < stock || header[2] != stock + std::uintptr_t{ts::stock_count} * list.entry_size) continue; + const auto used = header[1] - header[0]; + const auto entries = manager + list.raised_entries; + std::memcpy(reinterpret_cast(entries), reinterpret_cast(stock), used); + header[0] = entries; + header[1] = entries + used; + header[2] = entries + std::uintptr_t{ts::raised_count} * list.entry_size; + ++moved; + } + lists_moved.store(moved); + return moved == std::size(ts::lists); + } __except (EXCEPTION_EXECUTE_HANDLER) { + return false; + } +} +__declspec(noinline) void say_made(bool moved) noexcept { + try { + if (moved) + logging::log(logging::Level::info, logging::Channel::runtime, + "Texture streaming: the game can hold {} streamed textures at once (its own limit is {}).", ts::raised_count, ts::stock_count); + else + logging::log(logging::Level::error, logging::Channel::runtime, + "Texture streaming: the limit was raised but {} of the manager's {} lists were not where expected; they keep {} entries.", + std::size(ts::lists) - lists_moved.load(), std::size(ts::lists), ts::stock_count); + } catch (...) {} +} +void* made(void* manager, void* arena) { + auto* result = original(manager, arena); + if (raised.load() && result) say_made(move_lists(reinterpret_cast(result))); + return result; +} + +// Three of the manager's functions keep 20480 entries on their own stack, which more slots can +// outgrow (texture_streaming.h). Each is hooked and left exactly as it is up to that many. +using Filter = void (*)(void* manager, std::uintptr_t* list); +using Sort = void (*)(std::uint32_t* begin, std::uint32_t* end, std::uint32_t* scratch); +Filter original_filters[2]{}; +Sort original_sort{}; + +// A longer list goes through the game's function a stretch at a time, each stretch presented as +// a list of its own ({begin, end, capacity end}), and what each keeps is closed up behind the +// last. The function only reads the list, empties entries in place and writes the kept ones back +// from `begin`, so a stretch is all it needs to see. +void filter_in_stretches(Filter filter, void* manager, std::uintptr_t* list) noexcept { + constexpr std::uintptr_t stretch = std::uintptr_t{ts::stock_count} * 8; + const auto begin = list[0], end = list[1]; + auto kept_end = begin; + for (auto at = begin; at < end;) { + const auto stop = std::min(end, at + stretch); + std::uintptr_t part[3]{at, stop, stop}; + filter(manager, part); + const auto kept = part[1] - part[0]; + if (kept && part[0] != kept_end) std::memmove(reinterpret_cast(kept_end), reinterpret_cast(part[0]), kept); + kept_end += kept; + at = stop; + } + list[1] = kept_end; +} +template void filtered(void* manager, std::uintptr_t* list) { + const auto filter = original_filters[Index]; + if (!list || list[1] < list[0] || (list[1] - list[0]) / 8 <= ts::stock_count) return filter(manager, list); + filter_in_stretches(filter, manager, list); +} +// The sort wants scratch room for as many entries as it sorts; its one caller gives it a +// stack array of 20480. More than that get room of their own for the call. +void sorted(std::uint32_t* begin, std::uint32_t* end, std::uint32_t* scratch) { + const auto bytes = reinterpret_cast(end) - reinterpret_cast(begin); + if (end < begin || bytes / 8 <= ts::stock_count) return original_sort(begin, end, scratch); + auto* room = static_cast(HeapAlloc(GetProcessHeap(), 0, bytes)); + if (!room) return; // (unsorted this once: the order only says which textures are dealt with first) + original_sort(begin, end, room); + HeapFree(GetProcessHeap(), 0, room); +} +bool hook(std::uintptr_t base, const game::build::Fingerprint& print, void* replacement, void** kept) noexcept { + std::array head{}; + const auto target = reinterpret_cast(base + print.rva); + if (!memory::peek(base + print.rva, head) || head != print.bytes || hook_prepare(target, replacement, kept) != HookOk || + hook_enable(target) != HookOk) { + *kept = nullptr; + return false; + } + return true; +} + +// "texture_limit_stock.txt" beside the game: the limit is left as the game has it (to compare). +bool stock_asked() noexcept { + wchar_t path[MAX_PATH]{}; + const auto size = GetModuleFileNameW(nullptr, path, MAX_PATH); + if (!size || size >= MAX_PATH) return false; + std::wstring file(path, size); + const auto slash = file.find_last_of(L"/\\"); + if (slash == std::wstring::npos) return false; + file.resize(slash + 1); + file += L"texture_limit_stock.txt"; + return GetFileAttributesW(file.c_str()) != INVALID_FILE_ATTRIBUTES; +} + +void say(logging::Level level, const char* text) noexcept { + try { + logging::log(level, logging::Channel::runtime, "Texture streaming: {}", text); + } catch (...) {} +} +} // namespace + +bool install(std::uintptr_t base) noexcept { + if (installed.exchange(true)) return raised.load(); + if (!base) return false; + image = base; + if (stock_asked()) { + say(logging::Level::warning, "texture_limit_stock.txt is there; the game keeps its own limit of streamed textures."); + return false; + } + // Everything is checked before anything is changed: the constructor, and each instruction + // byte for byte. + std::array head{}; + if (!memory::peek(base + ts::constructor.rva, head) || head != ts::constructor.bytes) { + say(logging::Level::warning, "this build's manager differs; the game keeps its own limit of streamed textures."); + return false; + } + std::uintptr_t first = ~std::uintptr_t{}, last{}; + for (const auto& site : ts::sites) { + std::array now{}; + if (site.size > now.size() || site.at + 4u > site.size || !memory::peek_bytes(base + site.rva, now.data(), site.size) || + std::memcmp(now.data(), site.bytes, site.size) != 0) { + say(logging::Level::warning, "this build's streaming code differs; the game keeps its own limit of streamed textures."); + return false; + } + first = std::min(first, base + site.rva); + last = std::max(last, base + site.rva + site.size); + } + for (const auto& rewrite : ts::renderer_rewrites) { + std::array now{}; + if (!memory::peek(base + rewrite.rva, now) || std::memcmp(now.data(), rewrite.stock, now.size()) != 0) { + say(logging::Level::warning, "this build's renderer differs; the game keeps its own limit of streamed textures."); + return false; + } + first = std::min(first, base + rewrite.rva); + last = std::max(last, base + rewrite.rva + now.size()); + } + std::uintptr_t manager{}; + if (!memory::peek(base + ts::manager, manager) || manager) { + say(logging::Level::warning, "the game had made its manager already; it keeps its own limit of streamed textures."); + return false; + } + const auto target = reinterpret_cast(base + ts::constructor.rva); + if (hook_prepare(target, reinterpret_cast(&made), reinterpret_cast(&original)) != HookOk) { + original = nullptr; + say(logging::Level::warning, "the manager's constructor could not be hooked; the game keeps its own limit of streamed textures."); + return false; + } + // One span of pages made writable for all of it, so no write can fail half way through. + DWORD before{}; + const auto span = last - first; + if (!VirtualProtect(reinterpret_cast(first), span, PAGE_EXECUTE_READWRITE, &before)) { + say(logging::Level::warning, "the streaming code could not be made writable; the game keeps its own limit of streamed textures."); + return false; + } + for (const auto& site : ts::sites) { + const auto value = ts::raised(site.value, stock_value(site)); + std::memcpy(reinterpret_cast(base + site.rva + site.at), &value, 4); + } + for (const auto& rewrite : ts::renderer_rewrites) + std::memcpy(reinterpret_cast(base + rewrite.rva), rewrite.raised, sizeof(rewrite.raised)); + DWORD ignored{}; + VirtualProtect(reinterpret_cast(first), span, before, &ignored); + FlushInstructionCache(GetCurrentProcess(), reinterpret_cast(first), span); + raised.store(true); + const bool guarded = hook(base, ts::filter_loading, reinterpret_cast(&filtered<0>), reinterpret_cast(&original_filters[0])) & + hook(base, ts::filter_waiting, reinterpret_cast(&filtered<1>), reinterpret_cast(&original_filters[1])) & + hook(base, ts::sort_wanted, reinterpret_cast(&sorted), reinterpret_cast(&original_sort)); + if (!guarded) + say(logging::Level::error, "the limit was raised but a function with room for 20480 on its stack could not be hooked; past 20480 the game may crash."); + if (hook_enable(target) != HookOk) + say(logging::Level::error, "the limit was raised but the constructor's hook did not start; the manager's lists keep 20480 entries."); + return true; +} + +Use use() noexcept { + Use result; + result.limit = raised.load() ? ts::raised_count : ts::stock_count; + std::uintptr_t manager{}; + std::uint32_t bottom{}, top{}; + if (!image || !memory::peek(image + ts::manager, manager) || !manager || !memory::peek(manager + ts::taken_from_bottom, bottom) || + !memory::peek(manager + ts::taken_from_top, top) || top > result.limit || bottom > result.limit) + return result; + result.known = true; + result.taken = std::min(result.limit, bottom + (result.limit - top)); + return result; +} + +void tick() noexcept { + static ULONGLONG next{}; + static bool warned{}, full{}; + static std::uint32_t most{}; + const auto now = GetTickCount64(); + if (now < next) return; + next = now + 2000; + const auto state = use(); + if (!state.known) return; + const auto percent = static_cast(std::uint64_t{state.taken} * 100 / state.limit); + try { + // (A new high, in steps: how close a session came is worth having in a log.) + if (state.taken >= most + state.limit / 20) { + most = state.taken; + logging::log(logging::Level::info, logging::Channel::runtime, "Texture streaming: {} of {} streamed textures in use ({}%).", + state.taken, state.limit, percent); + } + if (state.taken >= state.limit && !full) { + full = true; + logging::log(logging::Level::error, logging::Channel::runtime, + "Texture streaming: all {} streamed textures are in use; the game hangs when it needs one more.", state.limit); + overlay::notify(overlay::NoticeLevel::error, "Too many textures loaded", + "The game is out of room for textures and may freeze. Uninstall some mods to play safely."); + } else if (percent >= 90 && !warned) { + warned = true; + logging::log(logging::Level::warning, logging::Channel::runtime, + "Texture streaming: {} of {} streamed textures in use ({}%); the game hangs at the limit.", state.taken, state.limit, percent); + overlay::notify(overlay::NoticeLevel::warning, "Close to the texture limit", + std::to_string(state.taken) + " of " + std::to_string(state.limit) + + " textures are loaded, and the game freezes at the limit. Do not install any more mods, or uninstall some."); + } else if (percent < 80) { + warned = full = false; // told again the next time it comes close + } + } catch (...) {} +} +} // namespace dingosdk::texture_handle_limit diff --git a/Extension/World/texture_handle_limit.h b/Extension/World/texture_handle_limit.h new file mode 100644 index 0000000..9b39d81 --- /dev/null +++ b/Extension/World/texture_handle_limit.h @@ -0,0 +1,22 @@ +#pragma once +#include + +// The game can stream 20480 textures at once and hangs when a map and its mods need more +// (Engine/Game/Build//texture_streaming.h). This raises the limit to 32764, the most a +// texture's handle can hold, by moving the manager's tables into a longer allocation before the +// game makes the manager; and it tells the player when the limit, raised or not, is close. +namespace dingosdk::texture_handle_limit { +// Checks every instruction this build is known by and changes them; once, before the game has +// made its texture streaming manager. False when the build differs or the manager is already +// there: the game keeps its own limit. +bool install(std::uintptr_t base) noexcept; + +struct Use { + bool known{}; // the manager is there + std::uint32_t limit{}; // 20480, or 32764 when raised + std::uint32_t taken{}; // slots the manager has handed out at its two ends +}; +Use use() noexcept; +// Client thread, every tick: a notice when the textures loaded come close to the limit. +void tick() noexcept; +} diff --git a/Runtime/client_tick.cpp b/Runtime/client_tick.cpp index 1b473c5..7ab7b5d 100644 --- a/Runtime/client_tick.cpp +++ b/Runtime/client_tick.cpp @@ -4,6 +4,7 @@ #include "Extension/Assets/live_mods.h" #include "Extension/Assets/map_download.h" #include "Extension/Assets/map_icons.h" +#include "Extension/World/texture_handle_limit.h" #include "Engine/Core/Log/logging.h" #include "Engine/Core/Profiling/profiler.h" #include "Extension/Settings/job_spin.h" @@ -1214,6 +1215,7 @@ void tick(std::uintptr_t client, std::uintptr_t update) { if (const auto need = dingosdk::multiplayer::take_map_need()) dingosdk::map_download::offer(*need); dingosdk::map_download::tick(); dingosdk::map_icons::tick(); + dingosdk::texture_handle_limit::tick(); } dingosdk::multiplayer::refresh_identity_lists(); // How much memory the game holds, beside what a session adds to it: said when it has diff --git a/Runtime/runtime_init.cpp b/Runtime/runtime_init.cpp index 8e6e295..727dc44 100644 --- a/Runtime/runtime_init.cpp +++ b/Runtime/runtime_init.cpp @@ -8,6 +8,8 @@ #include "Extension/Settings/replay_guard.h" #include "Extension/World/load_error_guard.h" #include "Extension/World/mesh_unload_guard.h" +#include "Extension/World/stack_check_report.h" +#include "Extension/World/texture_handle_limit.h" #include "Extension/Console/commands.h" #include "Extension/UI/ui_pointer_skip.h" #include "Engine/Game/Build/addresses.h" @@ -319,6 +321,8 @@ extern "C" __declspec(dllexport) BOOL WINAPI DingoSDKDebugInitialize() { (void)dingosdk::replay_guard::install(r.base); (void)dingosdk::mesh_unload_guard::install(r.base); (void)dingosdk::load_error_guard::install(r.base); + (void)dingosdk::texture_handle_limit::install(r.base); + (void)dingosdk::stack_check_report::install(r.base); dingosdk::console::start_memory_trace_from_environment(); const bool fixed_stop_provider = (global_offline_hook && progression_guard_armed) && dingosdk::prepare_fixed_stop_entitlement_provider(r.base); diff --git a/cmake/Runtime.cmake b/cmake/Runtime.cmake index b282683..14660fb 100644 --- a/cmake/Runtime.cmake +++ b/cmake/Runtime.cmake @@ -72,6 +72,8 @@ add_library(dingosdk_runtime SHARED Extension/Settings/replay_guard.cpp Extension/World/load_error_guard.cpp Extension/World/mesh_unload_guard.cpp + Extension/World/stack_check_report.cpp + Extension/World/texture_handle_limit.cpp Extension/Settings/replay_drafts.cpp Extension/UI/ui_pointer_skip.cpp Extension/Settings/named_settings.cpp