// ============================================================================ // Ishimura Patch (ishimurapatch.asi) - Dead Space (2023) // ---------------------------------------------------------------------------- // It times the game's D3D12 GPU memory requests and the residency waits // inside them, and logs the frames they slow down. 0.2.0 adds the first fix, // SUBALLOCATION (off by default, F9 switches it live): the game's small // committed resources are created as placed resources inside a few large // pre-made heaps instead. // // Why: at streaming boundaries (the lift test spot) the game creates // hundreds of small GPU resources in one frame. D3D12 makes each one // resident at creation and blocks the creating thread on the paging fence // (D3D12Core!CDevice::MakeResident_Worker -> PerNodePagingOperation::Wait), // about 0.3 ms of frame per request (ETW trace, 10/18). This build splits // that cost into the request and the wait, per thread and per frame, before // any fix is attempted. // // What is hooked (none of it is game code; Dead Space.exe is Denuvo- // protected and stays untouched): // - d3d12.dll!D3D12CreateDevice (MinHook), to find the device. // - The device's vtable: every Create*Resource / CreateHeap* variant, // MakeResident, Evict, EnqueueMakeResident. Slots come from vtindex.c. // - Present / Present1 (MinHook, on the code), the frame boundary, found // through a throwaway 8x8 swap chain on a hidden window. // - D3DKMTWaitForSynchronizationObjectFromCpu (MinHook), the kernel wait // the residency fence goes through. // // Suballocation: in-game (0.1.1, lift pass) the worst frame was 98.8 ms, // 447 committed creates at ~0.2 ms each (60% of it the residency wait) on // one job thread, next to 438 placed creates at ~0.01 ms each. Over a // session the game made 20,622 committed creates (6.8 s of thread time), // nearly all DEFAULT-heap, CREATE_NOT_ZEROED, flags 0 or UAV, <= 4 MB. Only // that shape is redirected: NOT_ZEROED means reused memory is acceptable, // and no render targets or depth buffers means no placed-resource // initialization rules apply. A resource's pages go back to the pool when // D3D12 destroys it (a tracker set with SetPrivateDataInterface is released // then). Anything that fails falls back to the committed path. // 0.2.1, from the 0.2.0 A/B: with the fix on, the creates left committed // were textures over 4 MB (~1 ms each, 389 ms a minute) and 64 KB READBACK // buffers in batches of ~50 (15-17 ms frames). The size cap is now 16 MB, // UPLOAD and READBACK buffers get their own pools, and blocks are made // ahead of need on a background thread instead of mid-burst. // // Logging (ishimurapatch.ini), ordered by detail as in Mouse Fix: 1 = what // was hooked and any errors, then quiet. 2 = also a HITCH line per slow // frame, a STATS line every 30 s, the queues, pool growth, the player's keys // and live ini changes (Log2). 3 = also every request and every frame, // written by a background thread from a lock-free ring so the hooks never // touch the file; the capture key saves the last minute to its own file. // ============================================================================ #define WIN32_LEAN_AND_MEAN #include #include #include #include #include #include #include #include #include #include #include #include "third_party/minhook/include/MinHook.h" #pragma comment(lib, "user32.lib") #pragma comment(lib, "version.lib") #pragma comment(lib, "winmm.lib") // PlaySound, for the toggle chime // The version only changes when a version is released. Which build this is // - the commit it was built from, uncommitted changes, when - comes from // build.bat (build_id.h) and the compiler, so every log names its exact build. static const char* MOD_VERSION = "0.3.9"; #if __has_include("build_id.h") #include "build_id.h" #else #define BUILD_ID "unknown" #endif static const char* BUILD_STAMP = BUILD_ID ", " __DATE__ " " __TIME__; extern "C" { extern const int VT_GetResourceAllocationInfo, VT_CreateCommittedResource, VT_CreateHeap, VT_CreatePlacedResource, VT_CreateReservedResource, VT_MakeResident, VT_Evict, VT_GetDeviceRemovedReason, VT_EnqueueMakeResident, VT_CreateCommittedResource1, VT_CreateHeap1, VT_CreateReservedResource1, VT_CreateCommittedResource2, VT_CreatePlacedResource1, VT_CreateCommittedResource3, VT_CreatePlacedResource2, VT_CreateReservedResource2, VT_Present, VT_Present1, VT_CreateCommandQueue, VT_CreateCommandQueue1, VT_Q_ExecuteCommandLists, VT_Q_Signal, VT_Q_Wait; } // ---------------------------------------------------------------------------- // Files and config // ---------------------------------------------------------------------------- struct Files { HMODULE module = NULL; char iniPath[MAX_PATH] = {}; char logPath[MAX_PATH] = {}; char marksPath[MAX_PATH] = {}; SYSTEMTIME started = {}; // when the mod loaded, for the marks file's session header bool iniCreated = false; }; static Files g_files; struct Config { int logging = 2; int hitchMs = 20; char captureKey[32] = "F8"; int suballocate = 1; char toggleKey[32] = "F9"; int blockMB = 128; int prewarmBlocks = 4; int maxResourceKB = 16384; int maxPoolMB = 4096; int hostBlockMB = 32; int maxHostPoolMB = 512; int probe = 0; int reflex = 0; int dataReadMBs = 0; int maxFps = 0; int raiseDirectQueue = 0; int subGpuTextures = 0; int raiseCopyQueue = 1; int crashInfo = 1; struct { char cls[96], fld[96], val[64]; } engine[16]; // [Engine] Class.Field = value int nEngine = 0; }; static Config g_cfg; // Test builds (build.bat test, TEST_BUILD) go to playtesters: the ini is five // keys with neutral names in one [Settings] section (everything else keeps // its default), the engine setting is one switch, StreamingFix, whose field // name is kept out of the binary's plain text, and the log says neutral words // (Neutral(), below; the word list is in context/test-build-words.md). #ifdef TEST_BUILD #define INI_FIX "Settings" #define INI_ADV "Settings" #define KEY_SUBALLOCATE "MemoryFix" #define BUILD_KIND " test build" #else #define INI_FIX "Fix" #define INI_ADV "Advanced" #define KEY_SUBALLOCATE "Suballocate" #define BUILD_KIND "" #endif #ifdef TEST_BUILD template struct Hidden { char b[N]; constexpr Hidden(const char (&s)[N]) : b{} { for (size_t i = 0; i < N; ++i) b[i] = (char)(s[i] ^ (0x5A + 7 * i)); } void Get(char* out) const { for (size_t i = 0; i < N; ++i) out[i] = (char)(b[i] ^ (0x5A + 7 * i)); } }; static constexpr Hidden STREAMING_FLD("ForceLoadStreamingImmediate"); static const char* STREAMING_CLS = "TextureStreamingSettings"; // in the binary anyway (frostbite.h SHAPES) static const char* DEFAULT_INI = "[Settings]\n" "MemoryFix = 1\n" "StreamingFix = 1\n" "ToggleKey = F9\n" "CaptureKey = F8\n" "Logging = 2\n"; #else static const char* DEFAULT_INI = "; ============================================================================\n" "; Dead Space (2023) Ishimura Patch - configuration\n" ";\n" "; Fixes the traversal stutter at streaming boundaries (walking off a lift,\n" "; through doors, down hallways). Nothing on disk is changed.\n" ";\n" "; Every setting here is optional. If you run into problems, delete this\n" "; file and it will be recreated with defaults on the next launch.\n" ";\n" "; Edit with any text editor. Changes take effect within about a second\n" "; of saving - even while the game is running - except the settings marked\n" "; (read at launch). Check ishimurapatch.log (same folder) to confirm what\n" "; was applied.\n" "; ============================================================================\n" "\n" "; ============================================================================\n" "; FIXES\n" "; ============================================================================\n" "\n" "[Fix]\n" "\n" "; Most of the stutter comes from the game asking Windows for hundreds of\n" "; small GPU memory blocks in one frame, each one a slow round trip. With\n" "; this on, those small textures and buffers are carved out of a few large\n" "; blocks made in advance instead. (Lift test: worst frame ~106 -> ~18 ms.)\n" "; Switching it applies to textures made from then on.\n" "; 1 = fixed (default), 0 = stock game behavior\n" "Suballocate = 1\n" "\n" "; Press this key while playing to switch Suballocate on or off, for\n" "; comparing the two in one session. A rising chime means on, a falling one\n" "; off, and the new value is saved to Suballocate above. F1-F12 or a\n" "; letter; empty = off.\n" "ToggleKey = F9\n" "\n" "; (read at launch) At a streaming boundary the game's copy queue uploads\n" "; the new area's data while the graphics queue waits for those uploads.\n" "; This creates the copy queue at high priority so they finish sooner.\n" "; (Lift test: about 25% fewer uneven frames.)\n" "; 1 = on (default), 0 = the game's normal priority\n" "RaiseCopyQueue = 1\n" "\n" "; (read at launch) The large blocks in graphics memory: their size (MB,\n" "; 16-256), how many are made when the game starts (0-16; more are added\n" "; in the background before they're needed), and the most they may total\n" "; (MB, 256-16384). At that limit, further requests are left as the game\n" "; made them, so this caps the extra graphics memory the fix can hold.\n" "BlockMB = 128\n" "PrewarmBlocks = 4\n" "MaxPoolMB = 4096\n" "\n" "; (read at launch) The largest single resource carved out of a block (KB,\n" "; 64-16384; never more than half a block). Larger ones stay as the game\n" "; made them.\n" "MaxResourceKB = 16384\n" "\n" "; (read at launch) The same for the small buffers the CPU and GPU pass\n" "; data through (system memory, not graphics memory): block size (MB,\n" "; 4-64) and the most they may total (MB, 16-2048).\n" "HostBlockMB = 32\n" "MaxHostPoolMB = 512\n" "\n" "\n" "[FrameRate]\n" "\n" "; Caps the frame rate. At the game's streaming boundaries the frames that\n" "; reach the screen late are the ones where the GPU has extra work to do;\n" "; with your frame rate pinned at the GPU's limit there is no room for it.\n" "; A cap a little below what the GPU can hold gives it that room. The wait\n" "; happens right after a frame is handed to Windows, so the next frame\n" "; starts after it and every frame is the same length. Turn off any other\n" "; frame cap (NVIDIA control panel, RTSS) or set it higher than this one.\n" "; 0 = off (default), otherwise the cap in frames per second (20-2000).\n" "MaxFps = 0\n" "\n" "\n" "[Reflex]\n" "\n" "; (read at launch) NVIDIA Reflex, which the game runs with no menu option.\n" "; It paces each frame to keep the queue of finished frames empty, so every\n" "; small hitch reaches the screen; without it, most of the frames left over\n" "; from a streaming boundary don't (lift test: half as many long frames on\n" "; screen, for about 0.5 ms more latency at a 163 fps cap). 0 keeps NVIDIA's\n" "; Reflex plugin (sl.reflex.dll) from loading; the file is not touched.\n" "; 0 = off (default), 1 = the game's choice\n" "Reflex = 0\n" "\n" "\n" "; ============================================================================\n" "; ENGINE SETTINGS - the game's own options, set in the running game\n" "; ============================================================================\n" "\n" "[Engine]\n" "\n" "; Written into the running game a few seconds after it starts, and kept\n" "; that way (logged). One per line, as Class.Field = value. Delete a line,\n" "; or put ; in front of it, and the game's own value is put back.\n" ";\n" "; ForceLoadStreamingImmediate: the game normally stops a frame to finish\n" "; loading a texture it needs right now. Off, the frame goes on with a\n" "; slightly lower-detail version for a few frames until the sharp one is\n" "; in. With [Fix] on this takes the lift test's worst frame ~18 -> ~12 ms.\n" "TextureStreamingSettings.ForceLoadStreamingImmediate = 0\n" "\n" "\n" "[Advanced]\n" "\n" "; Write a log to ishimurapatch.log (a fresh file every launch). At 128 MB\n" "; it becomes ishimurapatch.previous.log and a new one starts, so a long\n" "; session keeps the latest 128-256 MB.\n" "; 0 = off\n" "; 1 = what was hooked and any errors, then nothing more\n" "; 2 = (default) also one HITCH line for every frame at least HitchMs\n" "; long, a STATS line every 30 seconds, and the mark key's MARK lines.\n" "; About 1 MB an hour; fine for long sessions.\n" "; 3 = everything (read at launch): also every GPU memory request and\n" "; every frame, stamped in ms since the game started. Roughly 10-15 MB\n" "; a minute; turn it on to reproduce a problem, then send the log.\n" "Logging = 2\n" "\n" "; A frame at least this long (ms) gets a HITCH line (Logging 2 and 3).\n" "HitchMs = 20\n" "\n" "; Press this key right after a stutter or anything odd (Logging 2 and 3):\n" "; a short beep, a numbered line with the time in ishimurapatch_marks.txt\n" "; for you to write what happened after it (the file is added to, never\n" "; replaced), and a matching MARK line in the log. At Logging = 3 the last\n" "; minute of the log is also saved to its own file,\n" "; ishimurapatch_capture__