mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
feat(texture): dimension gate — NPOT textures, bound follows the scratch
1.12 requires power-of-two textures at two software sites (allocator FUN_00448450 and create validator FUN_0058AB10); 3.3.5 and 4.3.4 keep the rule unchanged. Verified in-game that the D3D backend samples NPOT correctly for the TGA/UI path, so Texture::DimensionGate relaxes both sites, default on. The real size ceiling is not the allocator's `< 0x400` but a single global decode scratch buffer sized once at boot (512^2 stock, 1024^2 with VanillaHelpers) and written with no bounds check. The validator's device-caps max is raw hardware and protects nothing — a 2560x1080 TGA faulted in the decoder's pixel writer at 0x004492E5. So the gate reads the scratch dimension back from the boot immediates and enforces exactly that, and every dimension failure — POT or size — routes through a 28-byte code cave that applies the bound before the fresh-allocation tail. Routing POT failures straight to that tail would jump over the size CMPs. Also fixes VanillaHelpers' recycle-pool collision: it grows the table to 6x6 but leaves the index stride at 5, so 32x1024 shared a bucket with 64x32 (rendered nothing) and 64x1024 with 128x32 (worked by luck). Both index sites are rewritten to stride 6 when its 36-bucket table is detected. Lua: _classicapi_NonPowerOfTwoTextures([enable]), _classicapi_TextureLimits().
This commit is contained in:
+101
@@ -7385,6 +7385,107 @@ enum Offsets {
|
||||
FUN_GX_TEXFLAGS_INIT = 0x0058A980,
|
||||
FUN_TEXTURE_GET_RENDERABLE = 0x0044ACF0,
|
||||
|
||||
// --- Texture dimension gates (power-of-two / max size) --------------------
|
||||
// 1.12 rejects a non-power-of-two texture TWICE, and both are plain software
|
||||
// checks — no device caps are consulted for the POT rule at either site:
|
||||
// 1. the allocator/recycler FUN_00448450, which returns NULL outright
|
||||
// (`TEST w,w-1 / JNZ reject` per axis, then `CMP dim,0x400 / JAE`);
|
||||
// 2. the GX create validator FUN_0058AB10, which re-tests POT for every
|
||||
// texture type but 2 (type 0 = 2D, 1 = cubemap — the BLP loader passes
|
||||
// `width == height*6` as the type, and FUN_00542180-style atlas creates
|
||||
// pass 0).
|
||||
// Verified unchanged in both later clients: 3.3.5 FUN_00681d90 and 4.3.4
|
||||
// FUN_00840d30 carry the validator's POT test verbatim, same type-2
|
||||
// exemption. What they DO change is the allocator: its dimension test is
|
||||
// demoted to a pool-eligibility window (`dim - 0x20 < 0x1e1`, i.e. [32,512]),
|
||||
// so an out-of-range dimension skips the recycle pool and allocates fresh
|
||||
// rather than failing. That demotion is only safe there because 3.3.5 clamps
|
||||
// its device-caps max to the scratch size (1024) and lets the validator carry
|
||||
// the bound. 1.12's validator ALSO compares against the per-type caps max
|
||||
// (`caps + 0x60 + type*4`, caps via FUN_0058A230) — but that value is raw
|
||||
// hardware (a 2560x1080 passed it), so in 1.12 the allocator's hardcoded
|
||||
// 0x400 is the ONLY thing protecting the decode scratch described below. It
|
||||
// is not redundant. Do not port the demotion without porting a bound.
|
||||
//
|
||||
// The recycle pool is indexed by log2(dim >> 5) and its hit test compares
|
||||
// only flags/type/miplevels — never width/height — so an NPOT texture must
|
||||
// never reach it or it recycles a bucket of unrelated dimensions. Any patch
|
||||
// relaxing the POT rule therefore has to route the failures through a bound
|
||||
// check and THEN to the pool-skip site — never simply delete them, and never
|
||||
// straight to pool-skip (see CODE ORDER MATTERS below).
|
||||
FUN_GX_TEXTURE_ALLOC = 0x00448450,
|
||||
PATCH_TEXALLOC_POT_W_JMP = 0x004484AE, // 0F 85 rel32 -> VA_TEXALLOC_REJECT
|
||||
PATCH_TEXALLOC_POT_H_JMP = 0x004484B9, // 0F 85 rel32 -> VA_TEXALLOC_REJECT
|
||||
// `CMP EBX,imm32` (81 FB) / `CMP EAX,imm32` (3D) — the size bound per axis,
|
||||
// 0x400 stock. VanillaHelpers.dll leaves these and flips the jumps below
|
||||
// from 0F 83 (JAE) to 0F 87 (JA) so exactly-1024 passes.
|
||||
PATCH_TEXALLOC_SIZE_W_IMM = 0x004484C1,
|
||||
PATCH_TEXALLOC_SIZE_H_IMM = 0x004484CC,
|
||||
PATCH_TEXALLOC_SIZE_W_JMP = 0x004484C5, // 0F 83 rel32 -> VA_TEXALLOC_REJECT
|
||||
PATCH_TEXALLOC_SIZE_H_JMP = 0x004484D0, // 0F 83 rel32 -> VA_TEXALLOC_REJECT
|
||||
VA_TEXALLOC_POOL_SKIP = 0x0044864B, // "skip the pool, create fresh"
|
||||
VA_TEXALLOC_REJECT = 0x00448665, // XOR EAX,EAX; RET 0x14
|
||||
// CODE ORDER MATTERS: the POT jumps sit ABOVE the size CMPs, so a POT
|
||||
// failure retargeted straight to VA_TEXALLOC_POOL_SKIP jumps OVER the size
|
||||
// bound — an NPOT 3000x3000 would reach the decoder unbounded. Any NPOT
|
||||
// relaxation must route POT failures through a bound check first
|
||||
// (Texture::DimensionGate does it with a code cave). At the jump sites EBX =
|
||||
// width and EAX = height, EDI = 0 and the 9-dword descriptor at [EBP-0x2C]
|
||||
// is complete, so the pool-skip tail is enterable from any of them.
|
||||
//
|
||||
// Recycle-pool index arithmetic, `index = h_log + w_log * SIDE`, SIDE = 5
|
||||
// for the stock 5x5 (32..512) table. VanillaHelpers grows the table to 6x6
|
||||
// (its init-count immediate below reads 36 instead of 25) but leaves BOTH of
|
||||
// these at stride 5, so a log of 5 on either axis collides: 32x1024 shares a
|
||||
// bucket with 64x32, 64x1024 with 128x32 — and the bucket match never
|
||||
// compares w/h, so the wrong-size texture is silently recycled. Fix = rewrite
|
||||
// each 5-byte pair to `IMUL r,r,6; ADD r,other` (6B F6 06 03 F3 at the
|
||||
// allocator, 6B DB 06 03 D8 at free), which preserves whichever operand
|
||||
// stock treats as the row and only changes the stride. Stock's alloc and
|
||||
// free agree with each other, so the fixed pair does too.
|
||||
PATCH_TEXPOOL_INDEX_STRIDE_ALLOC = 0x00448529, // 8D 04 B3 03 F0 LEA EAX,[EBX+ESI*4]; ADD ESI,EAX
|
||||
PATCH_TEXPOOL_INDEX_STRIDE_FREE = 0x004487AD, // 8D 0C 98 03 D9 LEA ECX,[EAX+EBX*4]; ADD EBX,ECX
|
||||
PATCH_TEXPOOL_INIT_COUNT = 0x00447C45, // B9 imm32 — bucket count: 25 stock, 36 VanillaHelpers
|
||||
// The stock table lives at 0x00B05BE8 (25 x 12 bytes) and ends EXACTLY at
|
||||
// VAR_TEXTURE_DECODE_SCRATCH — an out-of-range bucket index reads the scratch
|
||||
// pointer as a list head. So the size gate may never admit a dimension the
|
||||
// pool can't index unless that dimension is also routed around the pool.
|
||||
FUN_GX_TEXTURE_VALIDATE = 0x0058AB10,
|
||||
// THE ACTUAL SIZE CEILING. FUN_00448BD0 runs once at startup and allocates a
|
||||
// SINGLE global decode scratch buffer, sized `FUN_005a4b80(2, 0x200, 0x200)`
|
||||
// — one 512x512 texture's worth. Every loader points the decoder at it
|
||||
// (`texObj + 0x120`; see FUN_0044A260's param_1==0 branch and FUN_0044A560),
|
||||
// and the decoder writes rows with no bounds check, so a texture larger than
|
||||
// this buffer overruns the heap. The allocator's `< 0x400` gate is therefore
|
||||
// NOT redundant with the validator's device-caps max — the caps max runs to
|
||||
// thousands, this buffer does not. VanillaHelpers.dll patches both 0x200
|
||||
// immediates to 0x400 in the same function it relaxes the size limit in;
|
||||
// the pairing is mandatory, not incidental. Verified by crash: a 2560x1080
|
||||
// TGA faulted in the decoder's pixel writer at 0x004492E5 (ACCESS_VIOLATION,
|
||||
// write) 2.6x past a 1024x1024 buffer.
|
||||
// Single caller (boot, 0x00402862); the matching free + NULL is the exit
|
||||
// teardown FUN_00448D30. No mid-session re-init path exists, and the five
|
||||
// readers all re-read the global at use time, so the pointer can be swapped
|
||||
// for a larger engine-allocated buffer without a hook.
|
||||
FUN_TEXTURE_DECODE_SCRATCH_ALLOC = 0x00448BD0,
|
||||
VAR_TEXTURE_DECODE_SCRATCH = 0x00B05D14, // void* — the buffer itself
|
||||
// The two size immediates the boot alloc feeds FUN_GX_FORMAT_IMAGE_BYTES:
|
||||
// `MOV EDX,imm32` is width (BA imm32), `PUSH imm32` is height (68 imm32).
|
||||
// Read them back (+1) to learn what the buffer was ACTUALLY sized to on this
|
||||
// DLL stack — 0x200 stock, 0x400 once VanillaHelpers has run.
|
||||
PATCH_TEXDECODE_SCRATCH_DIM_W = 0x00448BE1,
|
||||
PATCH_TEXDECODE_SCRATCH_DIM_H = 0x00448BDC,
|
||||
// __fastcall(ecx = format, edx = width, [stack] = height) -> total bytes for
|
||||
// the image plus its mip chain plus the per-level row-pointer table.
|
||||
FUN_GX_FORMAT_IMAGE_BYTES = 0x005A4B80,
|
||||
// The `NEG EAX; SBB EAX,EAX` (F7 D8 1B C0) pair inside the validator's isPOT
|
||||
// idiom `(x & (x-1)) -> NEG/SBB -> INC -> TEST/JZ`, once per axis. The two
|
||||
// axes share the final TEST/JZ with the type-2 branch, so the arithmetic is
|
||||
// the surgical patch point — neutering the shared JZ would also disable the
|
||||
// type-2 flags check.
|
||||
PATCH_TEXVALIDATE_POT_W = 0x0058AB7C,
|
||||
PATCH_TEXVALIDATE_POT_H = 0x0058AB8D,
|
||||
|
||||
// --- Inline-texture positioning (slice 1: measure/emit integration) --------
|
||||
// The 1.12 text pipeline builds a layout as a list of render "nodes" (one
|
||||
// per wrapped line); each node owns per-font-page glyph vertex batches and a
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Texture dimension gates — make 1.12's allocator enforce the bound that
|
||||
// actually exists, and let non-power-of-two textures through it safely.
|
||||
//
|
||||
// THE INVARIANT. Every texture decode in this engine writes into ONE global
|
||||
// scratch buffer (VAR_TEXTURE_DECODE_SCRATCH), allocated once at boot for a
|
||||
// single square texture of side S (0x200 stock; 0x400 once VanillaHelpers has
|
||||
// patched the boot immediates) and written with no bounds check. So the rule
|
||||
// every client version upholds is: nothing may reach the decoder larger than
|
||||
// S on either axis. They just enforce it in different places:
|
||||
//
|
||||
// 1.12 allocator FUN_00448450: `< 1024` AND power-of-two, together => <= 512.
|
||||
// The create validator's device-caps max is raw hardware (thousands) and
|
||||
// protects nothing — a 2560x1080 sailed through it and faulted in the
|
||||
// decoder's pixel writer at 0x004492E5.
|
||||
// 3.3.5 scratch 1024; the allocator's size test is demoted to pool
|
||||
// eligibility ([32,512]) and the VALIDATOR carries the bound, because
|
||||
// 3.3.5 clamps its caps max to 1024. Verified in FUN_004b7f80 /
|
||||
// FUN_00681d90 / FUN_004b5bb0. Not grow-on-demand — the same fixed
|
||||
// buffer, one size up. VanillaHelpers' patch set is, in effect, this
|
||||
// exact backport (size 0x400, scratch 0x400, pool 6x6).
|
||||
//
|
||||
// Two consequences this module is built around. The POT rule was PART of 1.12's
|
||||
// size bound, so relaxing it alone exposes a 600x600 overrun on a stock client.
|
||||
// And the POT jumps sit ABOVE the size CMPs in the allocator, so retargeting a
|
||||
// POT failure straight to the pool-skip tail jumps OVER the bound.
|
||||
//
|
||||
// DESIGN. A 28-byte code cave in a page we own carries the bound:
|
||||
//
|
||||
// CMP EBX,N ; JA reject ; CMP EAX,N ; JA reject ; JMP pool-skip
|
||||
//
|
||||
// and every dimension failure in the allocator — POT or size — is routed into
|
||||
// it. The cave is the single source of truth for "too big"; the in-function
|
||||
// size CMPs decide only "poolable". Today both numbers are S. Layer 3 (large
|
||||
// textures) will grow the scratch, raise N in the cave, and leave the CMPs at
|
||||
// the pool's maximum — which is precisely 3.3.5's split between allocator and
|
||||
// validator, minus the dependence on a caps clamp 1.12 doesn't have.
|
||||
//
|
||||
// Layers shipped here:
|
||||
// 1. Gate follows the scratch (always on). S is read back from the boot
|
||||
// immediates, so the gate is right on any DLL stack, and additionally
|
||||
// capped to what the recycle pool can index (see PoolMaxDimension) —
|
||||
// the stock bucket table ends exactly at the scratch pointer.
|
||||
// 2. VanillaHelpers' pool-index stride (auto, when its 6x6 table is
|
||||
// detected). Their table is 6 wide but the index arithmetic stayed at
|
||||
// stride 5, so a 1024 dimension collides with a 32-high bucket and the
|
||||
// wrong-size texture is recycled: 32x1024 rendered nothing, 64x1024
|
||||
// worked by luck. Both index sites are rewritten to stride 6.
|
||||
// NPOT (default on, `_classicapi_NonPowerOfTwoTextures`). POT failures go to
|
||||
// the cave (bound, then fresh allocation — never the pool, whose log2
|
||||
// index is meaningless for NPOT), and the validator's own POT re-test is
|
||||
// forced true. Verified in-game for the TGA/UI path (clamped UVs, single
|
||||
// mip); NPOT BLPs with DXT blocks or mip chains are untested.
|
||||
//
|
||||
// Applied from module registration — glue boot and every FrameXML init — which
|
||||
// is after every other DLL's load-time patching, so the values read back are
|
||||
// the ones actually in force.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Texture::DimensionGate {
|
||||
|
||||
namespace {
|
||||
|
||||
// ---- byte helpers ------------------------------------------------------------
|
||||
|
||||
uint32_t ReadImm32(uintptr_t addr) { return *reinterpret_cast<const uint32_t *>(addr); }
|
||||
|
||||
void Put32(uint8_t *dst, uint32_t v) { std::memcpy(dst, &v, sizeof v); }
|
||||
|
||||
// rel32 for a jump whose NEXT instruction begins at `next`.
|
||||
uint32_t Rel32(uintptr_t next, uintptr_t target) {
|
||||
return static_cast<uint32_t>(static_cast<int32_t>(target - next));
|
||||
}
|
||||
|
||||
bool WriteImm32(uintptr_t site, uint32_t v) {
|
||||
return Common::PatchBytes(reinterpret_cast<void *>(site), &v, sizeof v);
|
||||
}
|
||||
|
||||
// Rewrite a 6-byte `0F <cond> rel32` near jump in engine code. The condition is
|
||||
// written explicitly, never preserved — VanillaHelpers may have flipped it.
|
||||
bool WriteNearJump(uintptr_t site, uint8_t cond, uintptr_t target) {
|
||||
uint8_t bytes[6] = {0x0F, cond, 0, 0, 0, 0};
|
||||
Put32(bytes + 2, Rel32(site + 6, target));
|
||||
return Common::PatchBytes(reinterpret_cast<void *>(site), bytes, sizeof bytes);
|
||||
}
|
||||
|
||||
// ---- what the engine was actually configured with ----------------------------
|
||||
|
||||
// Side of the boot-allocated decode scratch. Read back from the two immediates
|
||||
// FUN_00448BD0 feeds FUN_GX_FORMAT_IMAGE_BYTES, so this is what the buffer was
|
||||
// REALLY sized to, whoever patched them. A mismatch is someone else's bug; the
|
||||
// smaller is the only safe answer.
|
||||
uint32_t ScratchDimension() {
|
||||
const uint32_t w = ReadImm32(Offsets::PATCH_TEXDECODE_SCRATCH_DIM_W + 1);
|
||||
const uint32_t h = ReadImm32(Offsets::PATCH_TEXDECODE_SCRATCH_DIM_H + 1);
|
||||
return w < h ? w : h;
|
||||
}
|
||||
|
||||
// Recycle-pool side, from its init-count immediate: 25 = stock 5x5, 36 =
|
||||
// VanillaHelpers' 6x6. 0 = a layout we don't recognise.
|
||||
int PoolSide() {
|
||||
switch (ReadImm32(Offsets::PATCH_TEXPOOL_INIT_COUNT + 1)) {
|
||||
case 25:
|
||||
return 5;
|
||||
case 36:
|
||||
return 6;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Largest dimension the pool can index: buckets are keyed by log2(dim >> 5)
|
||||
// over 0..side-1. An unknown layout is treated as stock.
|
||||
uint32_t PoolMaxDimension(int side) { return 32u << ((side > 0 ? side : 5) - 1); }
|
||||
|
||||
// ---- the code cave -----------------------------------------------------------
|
||||
//
|
||||
// +0 81 FB imm32 CMP EBX, N ; EBX = width at every entry site
|
||||
// +6 0F 87 rel32 JA reject
|
||||
// +12 3D imm32 CMP EAX, N ; EAX = height
|
||||
// +17 0F 87 rel32 JA reject
|
||||
// +23 E9 rel32 JMP pool-skip ; fresh allocation, descriptor is complete
|
||||
//
|
||||
// Entered only by jumps from inside FUN_00448450, where EBX/EAX still hold the
|
||||
// dimensions, EDI is 0 and [EBP-0x2C] is the finished descriptor — everything
|
||||
// the pool-skip tail expects. Flags are clobbered; nothing downstream reads
|
||||
// them. Lives for the process: engine code points into it.
|
||||
|
||||
uint8_t *g_cave = nullptr;
|
||||
constexpr size_t kCaveSize = 28;
|
||||
|
||||
bool BuildCave(uint32_t bound) {
|
||||
if (g_cave == nullptr) {
|
||||
g_cave = static_cast<uint8_t *>(
|
||||
VirtualAlloc(nullptr, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE));
|
||||
if (g_cave == nullptr)
|
||||
return false;
|
||||
}
|
||||
const auto base = reinterpret_cast<uintptr_t>(g_cave);
|
||||
uint8_t c[kCaveSize];
|
||||
c[0] = 0x81, c[1] = 0xFB;
|
||||
Put32(c + 2, bound);
|
||||
c[6] = 0x0F, c[7] = 0x87;
|
||||
Put32(c + 8, Rel32(base + 12, Offsets::VA_TEXALLOC_REJECT));
|
||||
c[12] = 0x3D;
|
||||
Put32(c + 13, bound);
|
||||
c[17] = 0x0F, c[18] = 0x87;
|
||||
Put32(c + 19, Rel32(base + 23, Offsets::VA_TEXALLOC_REJECT));
|
||||
c[23] = 0xE9;
|
||||
Put32(c + 24, Rel32(base + 28, Offsets::VA_TEXALLOC_POOL_SKIP));
|
||||
std::memcpy(g_cave, c, kCaveSize);
|
||||
FlushInstructionCache(GetCurrentProcess(), g_cave, kCaveSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
uintptr_t Cave() { return reinterpret_cast<uintptr_t>(g_cave); }
|
||||
|
||||
// ---- state -------------------------------------------------------------------
|
||||
|
||||
uint32_t g_bound = 0; // what the cave rejects above
|
||||
uint32_t g_poolMax = 0; // what the in-function CMPs admit to the pool
|
||||
int g_poolSide = 0;
|
||||
bool g_strideFixed = false;
|
||||
bool g_nonPowerOfTwo = true;
|
||||
|
||||
// ---- layer 1: gate follows the scratch --------------------------------------
|
||||
|
||||
bool ApplySizeGate() {
|
||||
const uint32_t scratch = ScratchDimension();
|
||||
g_poolSide = PoolSide();
|
||||
const uint32_t poolMax = PoolMaxDimension(g_poolSide);
|
||||
// Until layer 3 routes oversize textures around the pool, the bound and the
|
||||
// pool limit are the same number: the smaller of what the scratch holds and
|
||||
// what the pool can index. (A VanillaHelpers install whose pool patch failed
|
||||
// but whose size patch didn't would otherwise index past the stock table —
|
||||
// straight into VAR_TEXTURE_DECODE_SCRATCH.)
|
||||
g_bound = scratch < poolMax ? scratch : poolMax;
|
||||
g_poolMax = g_bound;
|
||||
if (!BuildCave(g_bound))
|
||||
return false;
|
||||
bool ok = WriteImm32(Offsets::PATCH_TEXALLOC_SIZE_W_IMM, g_poolMax);
|
||||
ok = WriteImm32(Offsets::PATCH_TEXALLOC_SIZE_H_IMM, g_poolMax) && ok;
|
||||
// JA: strictly greater than the pool max leaves the pool path — so exactly
|
||||
// poolMax (1024 on a VanillaHelpers stack) remains poolable.
|
||||
ok = WriteNearJump(Offsets::PATCH_TEXALLOC_SIZE_W_JMP, 0x87, Cave()) && ok;
|
||||
ok = WriteNearJump(Offsets::PATCH_TEXALLOC_SIZE_H_JMP, 0x87, Cave()) && ok;
|
||||
return ok;
|
||||
}
|
||||
|
||||
// ---- layer 2: VanillaHelpers' pool-index stride -------------------------------
|
||||
|
||||
// `IMUL r,r,6 ; ADD r,other` in place of `LEA t,[other + r*4] ; ADD r,t`:
|
||||
// same length, same operand roles, stride 5 -> 6. See Offsets.h.
|
||||
constexpr uint8_t kStride6Alloc[5] = {0x6B, 0xF6, 0x06, 0x03, 0xF3}; // IMUL ESI,ESI,6; ADD ESI,EBX
|
||||
constexpr uint8_t kStride6Free[5] = {0x6B, 0xDB, 0x06, 0x03, 0xD8}; // IMUL EBX,EBX,6; ADD EBX,EAX
|
||||
|
||||
bool ApplyStrideFix() {
|
||||
if (g_poolSide != 6) {
|
||||
g_strideFixed = false;
|
||||
return true; // stock 5x5 (or unknown): stride 5 is correct, leave it
|
||||
}
|
||||
bool ok = Common::PatchBytes(reinterpret_cast<void *>(Offsets::PATCH_TEXPOOL_INDEX_STRIDE_ALLOC),
|
||||
kStride6Alloc, sizeof kStride6Alloc);
|
||||
ok = Common::PatchBytes(reinterpret_cast<void *>(Offsets::PATCH_TEXPOOL_INDEX_STRIDE_FREE),
|
||||
kStride6Free, sizeof kStride6Free) &&
|
||||
ok;
|
||||
g_strideFixed = ok;
|
||||
return ok;
|
||||
}
|
||||
|
||||
// ---- non-power-of-two ----------------------------------------------------------
|
||||
|
||||
struct Site {
|
||||
uintptr_t addr;
|
||||
unsigned len;
|
||||
};
|
||||
constexpr Site kNpotSites[] = {
|
||||
{Offsets::PATCH_TEXALLOC_POT_W_JMP, 6},
|
||||
{Offsets::PATCH_TEXALLOC_POT_H_JMP, 6},
|
||||
{Offsets::PATCH_TEXVALIDATE_POT_W, 4},
|
||||
{Offsets::PATCH_TEXVALIDATE_POT_H, 4},
|
||||
};
|
||||
constexpr int kNpotSiteCount = sizeof kNpotSites / sizeof kNpotSites[0];
|
||||
uint8_t g_npotOriginal[kNpotSiteCount][6];
|
||||
bool g_npotSnapshot = false;
|
||||
|
||||
// The validator computes "is a power of two" as
|
||||
// EAX = x & (x-1); NEG EAX; SBB EAX,EAX; INC EAX; TEST EAX,EAX; JZ fail
|
||||
// so replacing NEG/SBB with `XOR EAX,EAX` leaves the INC to produce 1 and the
|
||||
// test always passes. Patching the arithmetic rather than the shared JZ keeps
|
||||
// the type-2 branch — which reaches the same TEST through its own SBB — intact.
|
||||
constexpr uint8_t kPotTestForced[4] = {0x31, 0xC0, 0x90, 0x90}; // XOR EAX,EAX; NOP; NOP
|
||||
|
||||
// Nobody else patches these four sites, so the snapshot is stock — but reading
|
||||
// it back is still cheaper to trust than a byte pattern we assumed.
|
||||
void SnapshotNpotSites() {
|
||||
if (g_npotSnapshot)
|
||||
return;
|
||||
for (int i = 0; i < kNpotSiteCount; ++i)
|
||||
std::memcpy(g_npotOriginal[i], reinterpret_cast<const void *>(kNpotSites[i].addr),
|
||||
kNpotSites[i].len);
|
||||
g_npotSnapshot = true;
|
||||
}
|
||||
|
||||
bool ApplyNonPowerOfTwo(bool enable) {
|
||||
SnapshotNpotSites();
|
||||
bool ok = true;
|
||||
if (enable) {
|
||||
// JNZ (not a power of two) -> the cave: bound first, then a fresh
|
||||
// allocation — never the pool, whose log2 index cannot describe NPOT.
|
||||
ok = WriteNearJump(Offsets::PATCH_TEXALLOC_POT_W_JMP, 0x85, Cave()) && ok;
|
||||
ok = WriteNearJump(Offsets::PATCH_TEXALLOC_POT_H_JMP, 0x85, Cave()) && ok;
|
||||
ok = Common::PatchBytes(reinterpret_cast<void *>(Offsets::PATCH_TEXVALIDATE_POT_W),
|
||||
kPotTestForced, sizeof kPotTestForced) &&
|
||||
ok;
|
||||
ok = Common::PatchBytes(reinterpret_cast<void *>(Offsets::PATCH_TEXVALIDATE_POT_H),
|
||||
kPotTestForced, sizeof kPotTestForced) &&
|
||||
ok;
|
||||
} else {
|
||||
for (int i = 0; i < kNpotSiteCount; ++i)
|
||||
ok = Common::PatchBytes(reinterpret_cast<void *>(kNpotSites[i].addr), g_npotOriginal[i],
|
||||
kNpotSites[i].len) &&
|
||||
ok;
|
||||
}
|
||||
if (ok)
|
||||
g_nonPowerOfTwo = enable;
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Re-asserted on every registration so a `/reload` cannot leave the code
|
||||
// segment and our recorded state disagreeing. Order matters: the cave must
|
||||
// exist before anything is pointed at it.
|
||||
void ApplyCurrent() {
|
||||
if (!ApplySizeGate())
|
||||
return;
|
||||
ApplyStrideFix();
|
||||
ApplyNonPowerOfTwo(g_nonPowerOfTwo);
|
||||
}
|
||||
|
||||
// ---- Lua ---------------------------------------------------------------------
|
||||
|
||||
// `_classicapi_NonPowerOfTwoTextures([enable])` -> bool
|
||||
// Returns the state after the call; with no argument it only reports. The engine
|
||||
// caches textures by path for the session, so a texture that failed under the
|
||||
// old setting needs a different path to be retried.
|
||||
int __fastcall Script_NonPowerOfTwoTextures(void *L) {
|
||||
if (Game::Lua::Type(L, 1) > Game::Lua::TYPE_NIL)
|
||||
ApplyNonPowerOfTwo(Game::Lua::ToBoolean(L, 1) != 0);
|
||||
Game::Lua::PushBool(L, g_nonPowerOfTwo);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `_classicapi_TextureLimits()` -> maxDimension, poolSide, nonPowerOfTwo, strideFixed
|
||||
// What the gate is actually enforcing on this DLL stack, for /dump.
|
||||
int __fastcall Script_TextureLimits(void *L) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_bound));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_poolSide));
|
||||
Game::Lua::PushBool(L, g_nonPowerOfTwo);
|
||||
Game::Lua::PushBool(L, g_strideFixed);
|
||||
return 4;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
ApplyCurrent();
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_NonPowerOfTwoTextures",
|
||||
&Script_NonPowerOfTwoTextures);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_TextureLimits", &Script_TextureLimits);
|
||||
}
|
||||
|
||||
// The glue screen loads textures too, and this is the first registration that
|
||||
// runs after every other DLL's load-time patching.
|
||||
void RegisterGlue() { ApplyCurrent(); }
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
const Game::GlueModuleAutoRegister _glueAutoreg{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Texture::DimensionGate
|
||||
Reference in New Issue
Block a user