Skip to content

About

A texture replacement toolkit that allows for high-resolution texture replacements in DX9/11 games.

Resources

Stars

16 stars

Watchers

0 watching

Forks

Repository files navigation

Texture Toolkit

Latest release Build MIT

Texture Toolkit dumps and replaces textures at runtime in 32-bit and 64-bit Direct3D 9 and Direct3D 11 games on Windows. It is an .asi plugin, loaded by Ultimate ASI Loader (which itself goes in the game folder as dinput8.dll, d3d9.dll, dxgi.dll or another of its supported names). An in-game panel lists the textures in the current scene, shows their format and memory size, and lets you dump or replace them without restarting. See GAMES.md for the games it has been run in; that list records what has been tried, not what is supported, and any Direct3D 9 or Direct3D 11 game is in scope.

How it works

Texture Toolkit hooks the calls that create and upload textures: LockRect/UnlockRect on D3D9, and Map/Unmap plus CreateTexture2D on D3D11. When a texture's pixels are uploaded it computes a 64-bit hash over that data and writes the hash onto the resource as D3D private data. At draw time it reads the hash back from whatever texture the game binds (SetTexture on D3D9; PSSetShaderResources, VSSetShaderResources and CSSetShaderResources on D3D11); if a replacement exists for that hash, it substitutes it before the draw.

Storing the hash on the resource, instead of tracking raw pointers, keeps a replacement attached to the right texture after the driver frees an address and reuses it for something else. On D3D9 the tool also follows UpdateTexture, so art that the game loads into a SYSTEMMEM texture and copies into a DEFAULT-pool texture is matched by the copy the game actually renders.

Note

This project exists mainly for the purpose of being used with games that don't have native or community-developed texture modding tools, games that have limitations with texture modding, such as only fixed-resolution imports, or games that may have memory issues with direct game file texture modding. Games such as NFS: The Run and Bully: Scholarship had such limitations that inspired the creation of this tool.

Features

  • Direct3D 9 and Direct3D 11, both x86 and x64.
  • Textures that arrive through D3DX (D3DXCreateTextureFromFile*) are tracked as well as those uploaded with LockRect. Many Direct3D 9 games never lock a texture themselves, and hand the file to D3DX instead. Only a d3dx9_*.dll the game has already loaded is hooked.
  • DDS replacement: put <hash>.dds in TT/inject and it loads without a restart.
  • Texture mods: any other folder in TT is a mod, with a switch and a load order on the panel. See Texture mods.
  • Mip handling: a replacement is created with the mip count its own file carries. A single-level file replacing a mipmapped texture has its chain filled in when the format is uncompressed, and loads at its top level with a warning when it is not.
  • Dumping to TT/dump as .dds, with the full mip chain: automatically on load, one at a time from the panel, or every tracked texture at once.
  • 64-bit content hashing, so two identical textures share one hash and one replacement. The hash covers the texture's tightly-packed rows, not the driver's row padding, so a hash means the same thing on every machine and an inject folder can be shared as a mod.
  • Special K texture packs load unchanged: files named the way Special K names them (eight hex digits, the CRC-32C of the top mip) are recognised alongside our own, and show as "SK Injected" in the panel.
  • Blink in game: the texture selected in the panel flashes magenta in the game, so whatever it is drawn on can be found by eye.
  • Input isolation and a software cursor, so the game stops reading the mouse and keyboard while the panel is open.

The in-game panel

Press INSERT (or whatever HotKey is set to) to open it. A sidebar switches between four pages.

Textures lists everything the game has uploaded, with figures across the top for how many are tracked, injected, not yet applied, and dumped, and how much memory they take. Each row shows the hash, size, mip count, format, and status: injected (a replacement is on screen), SK injected (the same, from a file using Special K's naming), pending (an inject file exists and applies the next time the texture is drawn), failed (an inject file was refused; the log says why), dumped, or original. Click a column header to sort. The search box matches hash, dimensions, or format, in either spelling (BC3 or BC3_UNORM). Hover the list and press [ or ] to step through it.

Dumping lives here too: Auto-dump saves every texture to TT/dump as it loads, Dump all dumps what the list shows, and the folder button opens TT/dump. Beside the search box, Current scene only hides textures that are tracked but not being drawn (the page says how many, with one click to show them), and Skip under 16 x 16 leaves out tiny lookup tables and placeholders.

The inspector beside the list previews the selected texture on a checkerboard, so transparency reads as transparency, and Flip V / Flip H on the image turn the preview over for art a game stores upside down or mirrored. It shows the injected replacement, the live original while it is on screen, or the dumped .dds read back from disk. Below it are the dimensions, mip count, data size, format, the compressed and sRGB flags, and the D3D11 bind, usage, and misc flags. Copy the hash, dump the texture, or delete its dump from here, and drag the gap between the panes to resize them. With Blink in game on (the default), the selected texture blinks magenta in the game while the Textures page is open, so whatever it is drawn on can be found by eye.

Mod files is for replacements only: the switches for replacing textures and accepting Special K names, how many files were found, applied and refused, Reload, and the texture mods with their load order. Settings holds the overlay options and the folder locations, saved to the ini as they change. Diagnostics has "Log this frame", which writes every texture drawn in the next frame to the log, the verbose logging switch, and Build Info: the game, Windows, GPU, other software hooked into the game, and every setting, with a Copy button that puts it all on the clipboard for a bug report.

Building

Requires CMake 3.20 or newer, a Visual Studio toolchain with the Windows SDK, and git on PATH. Dear ImGui and MinHook are fetched automatically at configure time and pinned to the tags at the top of CMakeLists.txt, so a clean clone builds with no further setup.

32-bit (x86)

cmake -B build32 -A Win32
cmake --build build32 --config Release

Output: build32/Release/TextureToolkit-x86.asi.

64-bit (x64)

cmake -B build64 -A x64
cmake --build build64 --config Release

Output: build64/Release/TextureToolkit-x64.asi.

Match the build to the game: a 32-bit game needs the x86 build.

Installing

Download the latest .asi from the releases page. Both architectures are attached to every release and are built by CI from the tagged commit.

  1. Install Ultimate ASI Loader for the game if it does not have it already, then copy TextureToolkit-x86.asi (32-bit games) or TextureToolkit-x64.asi (64-bit games) into the game folder, or into its plugins/ or scripts/ folder. Texture Toolkit exports nothing a game imports, so it cannot be renamed into a proxy DLL itself; the ASI loader is that proxy.
  2. Launch the game. Texture Toolkit writes TextureToolkit.ini and its log next to the .asi, and creates a TT/ folder next to the executable containing dump/, inject/, and imgui.ini.
  3. Press INSERT to open the panel.

To replace a texture, read its hash from the panel (or dump it first), edit the .dds, and place it in TT/inject named after the hash, for example 5D3E2CCE1A7740B2.dds or 0x5D3E2CCE1A7740B2.dds. Dumps are written with their full mip chain, so an edited dump can go straight back into inject unchanged; if you author a block-compressed replacement yourself, export it with mipmaps. The TT folder name can be changed with ResourceRoot in the ini.

Configuration

TextureToolkit.ini is created next to the .asi on first run:

[TextureToolkit]
HotKey=0x2D
ResourceRoot=TT
EnableInjection=1
AutoDump=0
FilterSmallTextures=1
ShowCurrentFrameOnly=1
AcceptSpecialKNames=1
HighlightSelected=1
ShowOSDBanner=1
UIScale=0
Verbose=0
  • HotKey: virtual-key code that toggles the panel (0x2D INSERT, 0x24 HOME, 0x74 F5).
  • ResourceRoot: folder holding dump/, inject/, and imgui.ini; relative to the game folder, or an absolute path.
  • EnableInjection: load replacements from the inject/ folder.
  • AutoDump: dump every texture to the dump/ folder as it loads.
  • FilterSmallTextures: hide textures of 16x16 and smaller from the list and from Dump all. Textures under 16x16 are not tracked at all; mods still replace 16x16 ones.
  • ShowCurrentFrameOnly: list only textures drawn in the current scene.
  • HighlightSelected: blink the texture selected in the panel magenta, in the game (the inspector's "Blink in game").
  • AcceptSpecialKNames: also load files named the way Special K names them. Where both namings exist for the same texture, the file from the source higher in the load order is used; within one folder, our own naming wins.
  • ShowOSDBanner: show the startup banner.
  • UIScale: the panel's size. 0 (the default) follows the resolution, so the panel takes the same share of the screen everywhere: 1440p is 100%, 4K 150%, 1080p 75%. Any other value, such as 1.25, is used as is, between 0.5 and 4. Also set from the Settings page.
  • Verbose: write per-texture debug lines to the log; leave off for normal use, since it slows the game. It also writes a [Timing] line every five seconds: the average frame time and how many frames hitched (took over twice the usual time, and over 20 ms), and for each hook how long Texture Toolkit's own work in it took (calls, total, average, worst), so a stutter can be traced to us or ruled out. While a texture blinks in the game, a [Blink] line each second says how often the game drew it. In a Direct3D 11 game, [Diag] lines count how the game copies and uploads its textures and describe the first few it draws that were never tracked, which is what shows why a scene list stays empty. It can be switched from the panel's Diagnostics page.

Flipping a switch in the panel writes its new value back to this file, one key at a time, so comments and anything else you add by hand are kept. The mod load order and any mod you switch on or off from the panel are kept in two more sections, [Mods] and [ModEnabled]; see Texture mods.

Texture mods

TT/inject is for your own replacements. A mod you download, or one you publish, goes in a folder of its own next to it, and every folder in TT other than inject and anything whose name starts with dump is loaded as a mod (so a dump renamed to dump-garage is kept, not injected back):

TT/
  inject/          your own replacements
  DualShock/       a mod: its .dds files, in subfolders if it likes
    mod.ini        optional
  DarkMode/

Each mod shows on the panel's Mod files page with a switch to turn it on or off, and the buttons to move it up or down the load order. Where two sources ship a file for the same texture, the one higher in the list wins, and the page says when some of a mod's files are covered by one above it. TT/inject is in that list too. A mod the list has not seen before goes in at the top, so the one you installed last wins and applies in full straight away; after that it stays wherever you put it. If your own edits in TT/inject should beat a mod, move inject above it. Changes apply at once, and Reload replacements picks up a mod folder added while the game runs.

A mod can describe itself with a mod.ini in its folder. Every key is optional; without the file the folder name is shown and the mod is on. tools/mod.ini.example is a commented starting point: copy it into the mod's folder and rename it mod.ini.

[Mod]
Name=DualShock Button Prompts
Author=Someone
Version=1.2
Description=Replaces the keyboard prompts with PlayStation buttons.
; Whether the mod is on when first installed: 1/0, true/false, yes/no or on/off.
Enabled=1

What the panel changes is written to TextureToolkit.ini, and that always wins over the mod's own Enabled:

[Mods]
; Highest priority first. "inject" is TT/inject. Mods not listed load after the ones that are.
LoadOrder=inject;DualShock;DarkMode

[ModEnabled]
; Per mod folder, overriding its mod.ini Enabled.
DarkMode=0

If ResourceRoot is set to the game folder itself, only folders with a mod.ini are treated as mods, so the game's own folders are never scanned for textures.

Sharing a texture mod

A texture is identified by a 64-bit hash of its original pixel data, so a mod works on anyone else's copy of the same game. To publish one, put its .dds files in a folder with a mod.ini (see Texture mods) and tell people to drop that folder into TT with Texture Toolkit installed.

Two things decide whether a hash matches on someone else's machine:

  • Game version. A patch that reships texture assets changes their contents, and therefore their hashes. State the version you built against.
  • Texture quality settings. Some games upload a smaller top mip at lower settings, which is different pixel data and a different hash. State the setting you authored at.

Neither depends on the player's GPU or driver: the hash covers the texture's tightly-packed rows, never the driver's row padding, so it means the same thing on every machine.

Mip levels

Texture Toolkit builds a replacement with the mip count your file carries. Mip count is an authoring decision and is treated as one: a chain you deliberately stopped early is applied as authored, without complaint. The one case that is filled in for you is a single-level file replacing a mipmapped texture, which is equally what an author who meant it and an author who forgot the export checkbox would produce -- and only for uncompressed formats, where downsampling the source is cheap and clean.

For most world art, export with a full chain. A block-compressed replacement without mips samples its top level at every distance and shimmers in motion, and a higher-resolution replacement aliases more than the original did, not less. Mips cost about a third more VRAM. Dumps are written with their full chain, so an edited dump is already correct.

Stopping the chain early is the right call in specific places, and nothing here will argue with you: UI and HUD art drawn at or near 1:1 never samples below level 0 and pays VRAM for every level it carries, and some textures are authored against a known minimum on-screen size.

Anisotropic filtering is not a reason to ship fewer mips. Aniso exists to correct the over-blurring that mip sampling causes at grazing angles; dropping levels does not buy sharpness, it buys shimmer.

Why mips are not generated for compressed formats

Missing mips cannot be recovered from block-compressed data. Producing them means decompressing, downsampling, and re-encoding -- a second lossy pass over data that already took one -- so the generated levels come out visibly softer than the ones a proper exporter would have made from your uncompressed source. Texture Toolkit could pull in a BC encoder (DirectXTex) to do this automatically, and deliberately does not: it would spend a dependency and a quality penalty to paper over an export mistake, while also making it impossible to tell a deliberate short chain from a forgotten one. The assumption is that someone authoring texture replacements knows which compression and how many levels their texture wants. Export the mips you want; you will get them.

Compatibility

These are fixed. Changing any of them would rename every file in every published mod, so they are treated as a contract rather than an implementation detail:

  • The hash: 64-bit, computed over mip 0's tightly-packed rows, with the algorithm in TextureHash.h.
  • The filename: 16 uppercase hex digits plus .dds. A 0x prefix is also accepted.
  • The layout: <ResourceRoot>/inject and <ResourceRoot>/dump.

Special K's naming is accepted as a second key, never as a replacement for ours: an SK pack drops into inject/ and works, while files named our way keep working exactly as before. Adding a compatibility naming is additive by construction and cannot rename anything.

The set of pixel formats Texture Toolkit recognises is deliberately additive. A format it cannot positively identify is skipped rather than guessed at, so adding support for one later can only make new textures moddable -- it can never change a hash that already exists.

Limitations

  • Direct3D 9 and Direct3D 11 only. DirectX 8, 10, 12, and Vulkan are not hooked.
  • A DirectX 8 game run through a d3d8to9 wrapper renders as Direct3D 9, so the overlay appears, but its textures stay invisible. The wrapper feeds pixel data into the D3D9 textures through an internal path that never calls a LockRect, UpdateSurface, UpdateTexture, or StretchRect we can hook, so there is nothing to hash. Capturing those would require hooking Direct3D 8 directly, which is not implemented. With Verbose=1 the log fills with Hooked_CreateTexture lines and never a Tracked line.
  • Injection reads .dds only. Dumps are written as .dds.
  • A D3D9 texture in the default pool cannot be read back with LockRect, so the panel's Dump button fails on those; Auto-dump captures them from the upload instead.
  • The Special K checksum is taken as a texture loads, and only while Special K-named files are present. A Special K pack added to a session that had none applies to textures loaded after it, so restart the game, or reach a point where it reloads its textures.
  • Blink in game shows nothing for art the game draws once into an image it then reuses (some HUDs and menus); the verbose log says so. The magenta is tinted by whatever colour the game draws the texture with, so text that is drawn yellow flashes red.
  • Mips cannot be generated for block-compressed replacements; a single-level compressed file loads at its top level and aliases in motion. See Mip levels for why this is not done automatically.

License

MIT. See LICENSE.

About

A texture replacement toolkit that allows for high-resolution texture replacements in DX9/11 games.

Resources

Stars

16 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages