From 795fe8f087c286172ed0628505a3025e35ebff37 Mon Sep 17 00:00:00 2001 From: Torhan Bartel Date: Wed, 14 Oct 2026 00:47:03 +0200 Subject: [PATCH] Widescreen experiment: native render size via dxwrapper, classic menus - mods/widescreen: redirect the world view's reads of the 1024.0/768.0 constants and the 3D ortho extents to our variables; render size = monitor; relocate the world view's tile draw list (1750 entries, no bounds check) into an enlarged object (16384 entries). - mods/dxwrapper: route DirectDraw through dxwrapper (Dd7to9, D3D9) since Windows' D3D7 cannot render to targets larger than 2048; fix the game's device info GUID pointer that dxwrapper's enumeration leaves invalid. Pinned release fetched by CMake (hash-checked), installed next to the DLL. - Main menu (UI thread) and loading screen render in a 1024x768 viewport and are presented scaled ("classic" layout); in-game uses the full size. - re/patch: verified code patches (operand redirect, push immediates, displacements) and startup hooks; build.ps1/CMake download fixes. In-game view untested yet; HUD still at the top-left. Co-Authored-By: Claude Opus 5.5 (1M context) --- .gitignore | 1 + CMakeLists.txt | 7 +- build.ps1 | 4 +- cmake/DxWrapper.cmake | 40 ++++++ config/dxwrapper.ini | 152 +++++++++++++++++++++ docs/widescreen.md | 21 +++ src/game/addresses.h | 40 +++++- src/game/render/dx_driver7.h | 15 ++- src/game/ui/ui_manager.h | 10 ++ src/mods/borderless.cpp | 77 +++++++---- src/mods/dxwrapper.cpp | 116 ++++++++++++++++ src/mods/dxwrapper.h | 16 +++ src/mods/widescreen.cpp | 249 +++++++++++++++++++++++++++++++++++ src/mods/widescreen.h | 18 +++ src/re/config.cpp | 4 + src/re/config.h | 2 + src/re/dllmain.cpp | 2 + src/re/patch.cpp | 81 ++++++++++++ src/re/patch.h | 33 +++++ 19 files changed, 860 insertions(+), 28 deletions(-) create mode 100644 cmake/DxWrapper.cmake create mode 100644 config/dxwrapper.ini create mode 100644 src/game/ui/ui_manager.h create mode 100644 src/mods/dxwrapper.cpp create mode 100644 src/mods/dxwrapper.h create mode 100644 src/mods/widescreen.cpp create mode 100644 src/mods/widescreen.h create mode 100644 src/re/patch.cpp create mode 100644 src/re/patch.h diff --git a/.gitignore b/.gitignore index 05d5d4e..9c8a85d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ *.rea-snapshot sacred_re.ini /ghidra/ +/screenshots/ diff --git a/CMakeLists.txt b/CMakeLists.txt index cc05574..7966387 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,7 @@ set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") add_compile_definitions(WIN32_LEAN_AND_MEAN NOMINMAX _CRT_SECURE_NO_WARNINGS) include(cmake/Detours.cmake) +include(cmake/DxWrapper.cmake) set(SACRED_RE_CXX_FLAGS /W4 /permissive- /utf-8) @@ -26,14 +27,18 @@ add_library(sacred_re SHARED src/re/hook.cpp src/re/log.cpp src/re/config.cpp + src/re/patch.cpp src/re/sacred_re.def src/game/engine/log.cpp src/game/engine/cmdline.cpp src/mods/borderless.cpp + src/mods/widescreen.cpp + src/mods/dxwrapper.cpp ) target_include_directories(sacred_re PRIVATE src) target_compile_options(sacred_re PRIVATE ${SACRED_RE_CXX_FLAGS}) -target_link_libraries(sacred_re PRIVATE detours) +target_link_libraries(sacred_re PRIVATE detours ddraw) +sacred_re_install_dxwrapper(sacred_re) # --- sacred_re.exe: launches sacred.exe with sacred_re.dll injected ---------- add_executable(sacred_re_launcher WIN32 src/launcher/main.cpp) diff --git a/build.ps1 b/build.ps1 index 975e366..a992057 100644 --- a/build.ps1 +++ b/build.ps1 @@ -15,5 +15,7 @@ $env:PATH = "$(Split-Path $vswhere);$env:PATH" # re-configuring here would clash with VS Code's CMake Tools, which caches # the full path of cl.exe instead of the preset's plain "cl". $configure = if (Test-Path "build\$Preset\CMakeCache.txt") { "" } else { "cmake --preset $Preset && " } -cmd /c "`"$vcvars`" x86 >nul && $configure cmake --build --preset $Preset" +# 2>&1 inside cmd: Windows PowerShell turns any native stderr line (e.g. CMake's +# download progress) into a terminating error under ErrorActionPreference=Stop. +cmd /c "`"$vcvars`" x86 >nul && $configure cmake --build --preset $Preset 2>&1" if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/cmake/DxWrapper.cmake b/cmake/DxWrapper.cmake new file mode 100644 index 0000000..fb17ab8 --- /dev/null +++ b/cmake/DxWrapper.cmake @@ -0,0 +1,40 @@ +# dxwrapper (zlib; github.com/elishacloud/dxwrapper): translates the game's +# DirectDraw 7 / Direct3D 7 to Direct3D 9, lifting D3D7's 2048x2048 render +# target limit. Pinned release, verified by hash; installed next to +# sacred_re.dll in dxwrapper/ and loaded by src/mods/dxwrapper.cpp, so the +# game folder stays untouched. +include(FetchContent) + +# Some CMake builds (e.g. WinLibs/MinGW) ship curl without a CA store, so +# HTTPS downloads fail certificate verification. Fall back to Git for +# Windows' bundle when nothing else is configured. +if(NOT CMAKE_TLS_CAINFO AND NOT DEFINED ENV{SSL_CERT_FILE}) + file(TO_CMAKE_PATH "$ENV{ProgramFiles}" _program_files) + foreach(_bundle "${_program_files}/Git/mingw64/etc/ssl/certs/ca-bundle.crt" + "${_program_files}/Git/mingw64/ssl/certs/ca-bundle.crt") + if(EXISTS "${_bundle}") + set(CMAKE_TLS_CAINFO "${_bundle}") + break() + endif() + endforeach() +endif() + +FetchContent_Declare(dxwrapper_bin + URL https://github.com/elishacloud/dxwrapper/releases/download/v1.8.8600.25/dxwrapper.zip + URL_HASH SHA256=3894368ebce69f348d08e189c2f0818c49aa3b18ad01e06348743bf6a29af97a + DOWNLOAD_EXTRACT_TIMESTAMP TRUE +) +FetchContent_MakeAvailable(dxwrapper_bin) + +function(sacred_re_install_dxwrapper target) + set(_dest "$/dxwrapper") + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "${_dest}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${dxwrapper_bin_SOURCE_DIR}/Stub/ddraw.dll" + "${dxwrapper_bin_SOURCE_DIR}/dxwrapper.dll" + "${dxwrapper_bin_SOURCE_DIR}/License.txt" + "${CMAKE_SOURCE_DIR}/config/dxwrapper.ini" + "${_dest}" + VERBATIM) +endfunction() diff --git a/config/dxwrapper.ini b/config/dxwrapper.ini new file mode 100644 index 0000000..9e19c4c --- /dev/null +++ b/config/dxwrapper.ini @@ -0,0 +1,152 @@ +;; sacred_re: dxwrapper settings (copied next to dxwrapper.dll at build time). +;; Only change from the release defaults: Dd7to9 = 1 (DirectDraw/D3D7 -> D3D9). +;; Config file for DirectX DLL Wrapper +;; +;; For details, see: https://github.com/elishacloud/dxwrapper/wiki/Configuration +;; +[General] +RealDllPath = AUTO +WrapperMode = AUTO +LoadCustomDllPath = +ExcludeProcess = +IncludeProcess = +RunProcess = +WaitForProcess = 0 +DisableLogging = 0 + +[Plugins] +LoadPlugins = 0 +LoadFromScriptsOnly = 0 + +[Compatibility] +Dd7to9 = 1 +D3d8to9 = 0 +D3d9to9Ex = 0 +D3d9on12 = 0 +DDrawCompat = 0 +Dinputto8 = 0 +DisableGameUX = 0 +EnableDdrawWrapper = 0 +EnableD3d9Wrapper = 0 +EnableDinput8Wrapper = 0 +EnableDsoundWrapper = 0 +DisableGDIGammaRamp = 0 +EnableOpenDialogHook = 0 +WinVersionLie = off +WinVersionLieSP = 0 +ForceKeyboardLayout = 0 +FixPerfCounterUptime = 0 +HandleExceptions = 0 +SingleProcAffinity = 0 +ProcAffinityMask = 0 + +[DDrawCompat] +DDrawCompat20 = 0 +DDrawCompat21 = 0 +DDrawCompat32 = 0 +DDrawCompatDisableGDIHook = 0 +DDrawCompatNoProcAffinity = 0 + +[ddraw] +DdrawOverrideBitMode = 0 +DdrawUseDirect3D9Caps = 0 + +[Dd7to9] +DdrawAutoFrameSkip = 0 +DdrawEmulateSurface = 0 +DdrawEmulateLock = 0 +DdrawUseShadowSurface = 0 +DdrawKeepAllInterfaceCache = 0 +DdrawAllowMultiSampling = 0 +DdrawForceMipMapAutoGen = 0 +DdrawClampVertexZDepth = 0 +DdrawEnableByteAlignment = 0 +DdrawFixByteAlignment = 0 +DdrawIntroVideoFix = 0 +DdrawFilterActivateApp = 0 +DdrawRemoveScanlines = 0 +DdrawRemoveInterlacing = 0 +DdrawReadFromGDI = 0 +DdrawWriteToGDI = 0 +DdrawLimitTextureFormats = 0 +DdrawLimitDisplayModeCount = 0 +DdrawCustomWidth = 0 +DdrawCustomHeight = 0 +DdrawLinearTextureFilter = 0 +DdrawUseNativeResolution = 0 +DdrawOverrideWidth = 0 +DdrawOverrideHeight = 0 +DdrawOverrideStencilFormat = 0 +DdrawIntegerScalingClamp = 0 +DdrawMaintainAspectRatio = 0 + +[d3d9] +AnisotropicFiltering = 0 +AntiAliasing = 0 +EnableMultisamplingATOC = 0 +EnvironmentCubeMapFix = 0 +ForceMipMapUsage = 0 +DepthBiasFactor = 0 +DepthBiasDropOffValue = 0 +EnableVSync = 0 +ForceVsyncMode = 0 +ShowFPSCounter = 0 +OverrideRefreshRate = 0 +LimitRefreshRates = 0 +LimitDisplayModeCount = 0 +CustomDisplayWidth = 0 +CustomDisplayHeight = 0 +LimitPerFrameFPS = 0 +EnableWindowMode = 0 +WindowModeBorder = 0 +WindowModeGammaShader = 0 +DisplayBrightness = 0 +DisplayContrast = 0 +SetInitialWindowPosition = 0 +InitialWindowPositionLeft = 0 +InitialWindowPositionTop = 0 +FullscreenWindowMode = 0 +HideWindowFocusChanges = 0 +EnableCursorClip = 0 +ForceExclusiveFullscreen = 0 +ForceMixedVertexProcessing = 0 +ForceSystemMemVertexCache = 0 +UseShadowBackbuffer = 0 +OverrideStencilFormat = 0 +FlipEx = 0 +SetSwapEffectShim = 0 +DisableMaxWindowedMode = 0 +GraphicsHybridAdapter = 0 + +[FullScreen] +FullScreen = 0 +ForceWindowResize = 0 +WaitForWindowChanges = 0 + +[dinput8] +DeviceLookupCacheTime = 0 +FilterNonActiveInput = 0 +InvertForceDirection = 0 +FixHighFrequencyMouse = 0 +MouseMovementFactor = 0 +MouseMovementFactorX = 0 +MouseMovementFactorY = 0 +MouseMovementPadding = 0 +MouseMovementPaddingX = 0 +MouseMovementPaddingY = 0 + +[dsound] +Num2DBuffers = 0 +Num3DBuffers = 0 +ForceCertification = 0 +ForceExclusiveMode = 0 +ForceSoftwareMixing = 0 +ForceHardwareMixing = 0 +ForceHQ3DSoftMixing = 0 +ForceNonStaticBuffers = 0 +ForceVoiceManagement = 0 +ForcePrimaryBufferFormat = 0 +PrimaryBufferBits = 16 +PrimaryBufferSamples = 44100 +PrimaryBufferChannels = 2 +AudioClipDetection = 0 diff --git a/docs/widescreen.md b/docs/widescreen.md index 180e623..cf205da 100644 --- a/docs/widescreen.md +++ b/docs/widescreen.md @@ -59,6 +59,27 @@ Elsewhere (unknown / UI, leave alone for now): `0x40694a`, `0x4ec036`, `0x60c1f7`, `0x60da54`, `0x625039`, `0x6283b3`, `0x62858d`, `0x628d3b`. +## Other frame producers (laid out for 1024x768: "classic" frames) + +| Address | What | Thread | +|---|---|---| +| `0x00756940` | `cUI_Manager` thread loop: main menu (clear, update, BeginScene `0x646f50`, UI render `0x7587b0` incl. the menu's 3D scene, EndScene `0x646fd0`, Flip) | own UI thread | +| `0x006469d0` | loading screen: GDI BitBlt of bitmap + progress bar + text into the back buffer (fixed 1024x768) | render thread | +| `0x006a0c60` | video/intro playback (unexamined) | | + +The widescreen mod gives these frames a 1024x768 D3D viewport (set at +`BeginScene` when the scene depth `+0x1d8` is 0) and the borderless mod +presents only that area, scaled to the monitor. + +## Direct3D 7 render target limit + +Windows' D3D7 runtime refuses `IDirect3D7::CreateDevice` on any render +target wider or taller than 2048 (`DDERR_INVALIDOBJECT`, 0x88760082), +measured with a standalone test: 2048x1440 works, 2049x1440 / 2560x1080 / +3440x1440 fail. Native ultrawide therefore needs dxwrapper (`Dd7to9`, +D3D7 -> D3D9), see `src/mods/dxwrapper.cpp`; without it the render size is +clamped to 2048. + ## Renderer `DxDriver7::SetDisplayMode` (`0x00645390`) creates back buffer, z-buffer and diff --git a/src/game/addresses.h b/src/game/addresses.h index f8a52bc..d88539c 100644 --- a/src/game/addresses.h +++ b/src/game/addresses.h @@ -14,6 +14,10 @@ constexpr uint32_t GetMouseClientPos = 0x0066e500; constexpr uint32_t MainWindowProc = 0x008122a0; constexpr uint32_t DxDriver7_SetDisplayMode = 0x00645390; constexpr uint32_t DxDriver7_Flip = 0x00645f70; +constexpr uint32_t DxDriver7_BeginScene = 0x00646f50; +constexpr uint32_t DxDriver7_LoadingScreen = 0x006469d0; // GDI progress screen, 1024x768 +constexpr uint32_t UiManager_ThreadLoop = 0x00756940; // main menu render loop (own thread) +constexpr uint32_t Engine_RenderThreadLoop = 0x0060e3f0; // in-game render loop constexpr uint32_t WorldView0_SetupView = 0x00624ae0; constexpr uint32_t WorldView0_SetZoom = 0x00628210; constexpr uint32_t WorldView0_Render = 0x006322b0; @@ -39,8 +43,40 @@ constexpr uint32_t kViewHeightUsers[] = { 0x0062ea5e, 0x0062f345, 0x0063068a, 0x00630f77, }; -// push sequences of the 3D models' ortho extents (-267, 267, -200, 200). -constexpr uint32_t kOrthoExtentPushes[] = { +// cWorldView0 is allocated with `push 970F4h; call operator new` here. +constexpr uint32_t kWorldViewAllocSizePush = 0x006101aa; +constexpr uint32_t kWorldViewSize = 0x970f4; + +// cWorldView0's tile draw list: entries of 0x98 bytes at +0x3ff2c, count at +// +0x80e3c, room for ~1750 entries and no bounds check (enough for 1024x768 +// only). Instructions addressing it by displacement (+0x3ff2c + field): +constexpr uint32_t kTileListOffset = 0x3ff2c; +constexpr uint32_t kTileListEntrySize = 0x98; +constexpr uint32_t kTileListUsers[] = { + 0x0062b6d9, 0x0062b6ec, 0x0062b709, 0x0062b730, 0x0062b74b, 0x0062b763, // writer, row renderer 0x62b000 + 0x0062b777, 0x0062b797, 0x0062b7b7, 0x0062b7d7, + 0x0062df47, 0x0062e252, // readers (lea base pointer) +}; + +// Ortho projection of the 3D models: x -267..267, y -200..200 world units +// span the 1024x768 screen (times zoom). Doubles, read with `fmul qword ptr`. +constexpr uint32_t kOrthoLeftD = 0x0088ee40; // double -267.0 +constexpr uint32_t kOrthoRightD = 0x0088ee48; // double 267.0 +constexpr uint32_t kOrthoBottomD = 0x0088ee50; // double -200.0 +constexpr uint32_t kOrthoTopD = 0x0088ee58; // double 200.0 + +// World view instructions `fmul st, qword ptr [kOrtho*D]` (zoomed projection), +// four per site, in the order top, bottom, right, left. +constexpr uint32_t kOrthoZoomedUsers[][4] = { + {0x00624fdb, 0x00624ff3, 0x00625001, 0x0062500f}, + {0x00628355, 0x0062836d, 0x0062837b, 0x00628389}, + {0x0062852f, 0x00628547, 0x00628555, 0x00628563}, + {0x00628cd7, 0x00628cef, 0x00628d00, 0x00628d11}, +}; + +// Unzoomed projection pushed as immediates: 4 consecutive `push imm32` +// (5 bytes each) starting here, in the order top, bottom, right, left. +constexpr uint32_t kOrthoPushSequences[] = { 0x0060c1f7, 0x0060da54, 0x00625039, 0x006283b3, 0x0062858d, 0x00628d3b, }; diff --git a/src/game/render/dx_driver7.h b/src/game/render/dx_driver7.h index 4d88770..c1ae6d9 100644 --- a/src/game/render/dx_driver7.h +++ b/src/game/render/dx_driver7.h @@ -34,11 +34,16 @@ struct DxDriver7 { void* directDraw; // +0xb4 IDirectDraw7* void* primarySurface; // +0xb8 IDirectDrawSurface7* void* backBuffer; // +0xbc IDirectDrawSurface7* - uint8_t unknown0c0[0x11c]; + uint8_t unknown0c0[0x0c]; + void* device; // +0xcc IDirect3DDevice7* + uint8_t unknown0d0[0x108]; + int32_t sceneDepth; // +0x1d8 nesting of BeginScene/EndScene RECT blitSource; // +0x1dc back-buffer rect copied by Flip (windowed) RECT blitDest; // +0x1ec screen-space destination of that copy }; static_assert(offsetof(DxDriver7, window) == 0xb0); +static_assert(offsetof(DxDriver7, device) == 0xcc); +static_assert(offsetof(DxDriver7, sceneDepth) == 0x1d8); static_assert(offsetof(DxDriver7, blitSource) == 0x1dc); static_assert(offsetof(DxDriver7, blitDest) == 0x1ec); @@ -55,4 +60,12 @@ int __fastcall DxDriver7_SetDisplayMode(DxDriver7* self, void* edx, void* device // primary->Blt(blitDest, backBuffer, blitSource) in windowed mode. int __fastcall DxDriver7_Flip(DxDriver7* self, void* edx); +// sacred.exe 0x00646f50. device->BeginScene() unless already inside a scene +// (counted in sceneDepth). Returns 1. +int __fastcall DxDriver7_BeginScene(DxDriver7* self, void* edx); + +// sacred.exe 0x006469d0. Draws the loading screen (bitmap + progress bar + +// text) into the back buffer with GDI at fixed 1024x768 coordinates, then Flip. +void __fastcall DxDriver7_LoadingScreen(DxDriver7* self, void* edx, uint32_t progress, const char* text); + } // namespace game diff --git a/src/game/ui/ui_manager.h b/src/game/ui/ui_manager.h new file mode 100644 index 0000000..e892593 --- /dev/null +++ b/src/game/ui/ui_manager.h @@ -0,0 +1,10 @@ +#pragma once + +namespace game { + +// sacred.exe 0x00756940. cUI_Manager's thread body: renders the main menu +// (clear, UI update, BeginScene, UI render 0x7587b0, EndScene, Flip) in a loop +// until the manager is stopped. In-game the UI is drawn by the render thread. +void __fastcall UiManager_ThreadLoop(void* self, void* edx); + +} // namespace game diff --git a/src/mods/borderless.cpp b/src/mods/borderless.cpp index 3794c8c..e45a1c7 100644 --- a/src/mods/borderless.cpp +++ b/src/mods/borderless.cpp @@ -26,12 +26,15 @@ // SetCursorPos by a game-pixel delta, which is not scaled to screen pixels. #include +#include #include #include #include "game/engine/input.h" #include "game/render/dx_driver7.h" +#include "mods/dxwrapper.h" +#include "mods/widescreen.h" #include "re/config.h" #include "game/addresses.h" #include "re/hook.h" @@ -39,17 +42,28 @@ namespace { -struct Viewport { - bool active = false; - HWND window = nullptr; - bool runInBackground = false; +// Where a gameWidth x gameHeight image appears in the window. +struct Layout { int gameWidth = 0; int gameHeight = 0; RECT client = {}; // scaled game image, window client coordinates RECT screen = {}; // same rectangle, screen coordinates }; +struct Viewport { + bool active = false; + HWND window = nullptr; + bool runInBackground = false; + Layout full; // the whole back buffer + Layout classic; // its top-left 1024x768 (menus, loading screen; widescreen mod) +}; + Viewport g_viewport; +std::atomic g_classicShown = false; // which layout the last presented frame used + +const Layout& CurrentLayout() { + return g_classicShown ? g_viewport.classic : g_viewport.full; +} bool Enabled() { return re::ConfigInt("borderless", "Enabled", 1) != 0; @@ -86,35 +100,43 @@ void ApplyBorderless(game::DxDriver7* driver) { SetWindowLongW(window, GWL_STYLE, WS_POPUP | WS_VISIBLE); SetWindowPos(window, HWND_TOP, area.left, area.top, areaWidth, areaHeight, SWP_FRAMECHANGED | SWP_SHOWWINDOW); + const bool integerScale = re::ConfigInt("borderless", "IntegerScale", 0) != 0; + auto makeLayout = [&](int width, int height) { + Layout layout; + layout.gameWidth = width; + layout.gameHeight = height; + layout.client = FitRect(areaWidth, areaHeight, width, height, integerScale); + layout.screen = layout.client; + OffsetRect(&layout.screen, area.left, area.top); + return layout; + }; + Viewport viewport; viewport.window = window; viewport.runInBackground = re::ConfigInt("borderless", "RunInBackground", 1) != 0; - viewport.gameWidth = driver->mode.width; - viewport.gameHeight = driver->mode.height; - viewport.client = FitRect(areaWidth, areaHeight, viewport.gameWidth, viewport.gameHeight, - re::ConfigInt("borderless", "IntegerScale", 0) != 0); - viewport.screen = viewport.client; - OffsetRect(&viewport.screen, area.left, area.top); + viewport.full = makeLayout(driver->mode.width, driver->mode.height); + viewport.classic = makeLayout(std::min(driver->mode.width, 1024), std::min(driver->mode.height, 768)); viewport.active = true; g_viewport = viewport; + g_classicShown = false; // Paint the bars: the window class background brush is black. InvalidateRect(window, nullptr, TRUE); - re::Log("borderless: %dx%d game -> %ldx%ld monitor, image at (%ld,%ld) %ldx%ld", viewport.gameWidth, - viewport.gameHeight, static_cast(areaWidth), static_cast(areaHeight), viewport.client.left, - viewport.client.top, viewport.client.right - viewport.client.left, - viewport.client.bottom - viewport.client.top); + const RECT& image = viewport.full.client; + re::Log("borderless: %dx%d game -> %ldx%ld monitor, image at (%ld,%ld) %ldx%ld", viewport.full.gameWidth, + viewport.full.gameHeight, static_cast(areaWidth), static_cast(areaHeight), image.left, + image.top, image.right - image.left, image.bottom - image.top); } // Window client coordinates -> game pixels. POINT ToGame(POINT p) { - const Viewport& v = g_viewport; - const int width = v.client.right - v.client.left; - const int height = v.client.bottom - v.client.top; + const Layout& layout = CurrentLayout(); + const int width = layout.client.right - layout.client.left; + const int height = layout.client.bottom - layout.client.top; POINT result; - result.x = std::clamp(MulDiv(p.x - v.client.left, v.gameWidth, width), 0, v.gameWidth - 1); - result.y = std::clamp(MulDiv(p.y - v.client.top, v.gameHeight, height), 0, v.gameHeight - 1); + result.x = std::clamp(MulDiv(p.x - layout.client.left, layout.gameWidth, width), 0, layout.gameWidth - 1); + result.y = std::clamp(MulDiv(p.y - layout.client.top, layout.gameHeight, height), 0, layout.gameHeight - 1); return result; } @@ -122,9 +144,9 @@ POINT ToGame(POINT p) { void PaintBars(HDC dc) { RECT window; GetClientRect(g_viewport.window, &window); + const RECT& image = CurrentLayout().client; const int saved = SaveDC(dc); - ExcludeClipRect(dc, g_viewport.client.left, g_viewport.client.top, g_viewport.client.right, - g_viewport.client.bottom); + ExcludeClipRect(dc, image.left, image.top, image.right, image.bottom); FillRect(dc, &window, static_cast(GetStockObject(BLACK_BRUSH))); RestoreDC(dc, saved); } @@ -146,11 +168,13 @@ RE_HOOK(addr::MainWindowProc, MainWindowProc); int __fastcall DxDriver7_SetDisplayMode(DxDriver7* self, void* edx, void* deviceCaps, const DxDisplayMode* mode, uint32_t unknown) { + mods::dxwrapper::FixDeviceInfo(deviceCaps); if (!Enabled()) { return DxDriver7_SetDisplayMode_original(self, edx, deviceCaps, mode, unknown); } DxDisplayMode windowed = *mode; windowed.fullscreen = 0; + mods::widescreen::ApplyRenderSize(windowed, self->window); g_viewport.active = false; const int result = DxDriver7_SetDisplayMode_original(self, edx, deviceCaps, &windowed, unknown); if (result) { @@ -163,8 +187,15 @@ int __fastcall DxDriver7_Flip(DxDriver7* self, void* edx) { // The original computes the blit rectangles once, for a centred window of // exactly the game's size; override them on every present. if (g_viewport.active && self->window == g_viewport.window) { - self->blitSource = {0, 0, g_viewport.gameWidth, g_viewport.gameHeight}; - self->blitDest = g_viewport.screen; + // Menus and the loading screen only fill the top-left 1024x768 of a + // widened back buffer; show just that part, scaled like before. + const bool classic = mods::widescreen::IsClassicFrame(); + if (g_classicShown.exchange(classic) != classic) { + InvalidateRect(self->window, nullptr, TRUE); // repaint the bars for the new layout + } + const Layout& layout = CurrentLayout(); + self->blitSource = {0, 0, layout.gameWidth, layout.gameHeight}; + self->blitDest = layout.screen; } return DxDriver7_Flip_original(self, edx); } diff --git a/src/mods/dxwrapper.cpp b/src/mods/dxwrapper.cpp new file mode 100644 index 0000000..1883447 --- /dev/null +++ b/src/mods/dxwrapper.cpp @@ -0,0 +1,116 @@ +// Route the game's DirectDraw 7 / Direct3D 7 through dxwrapper (Dd7to9: +// translation to Direct3D 9). Windows' own D3D7 runtime cannot render to +// surfaces larger than 2048x2048, which the widescreen mod needs. +// +// The build puts dxwrapper's ddraw.dll stub, dxwrapper.dll and our +// config/dxwrapper.ini into dxwrapper/ next to sacred_re.dll; the game folder +// is not touched. sacred.exe imports three functions from DDRAW.dll; we hook +// them and forward to the wrapper, which is loaded on the first call (never +// from DllMain). Calls made while the wrapper is running (it may use the real +// DirectDraw itself) go to the original functions. +// +// sacred_re.ini: +// [renderer] +// DxWrapper=1 ; 0 = Windows' DirectDraw (render size limited to 2048) + +#include "mods/dxwrapper.h" + +#include +#include +#include + +#include +#include + +#include "re/config.h" +#include "re/hook.h" +#include "re/log.h" + +namespace mods::dxwrapper { +namespace { + +HMODULE g_wrapper = nullptr; +bool g_loadAttempted = false; +thread_local int g_insideWrapper = 0; + +std::wstring WrapperPath() { + return re::ModuleDirectory() + L"dxwrapper\\ddraw.dll"; +} + +bool Enabled() { + return re::ConfigInt("renderer", "DxWrapper", 1) != 0; +} + +FARPROC WrapperFunction(const char* name) { + if (!g_loadAttempted) { + g_loadAttempted = true; + const std::wstring path = WrapperPath(); + g_wrapper = LoadLibraryW(path.c_str()); + if (g_wrapper) { + re::Log("dxwrapper: loaded %ls", path.c_str()); + } else { + re::Log("dxwrapper: could not load %ls (error %lu), using Windows' DirectDraw", path.c_str(), + GetLastError()); + } + } + return g_wrapper ? GetProcAddress(g_wrapper, name) : nullptr; +} + +// Calls `function` from the wrapper if possible, else `original`. +template +HRESULT Forward(const char* name, Function original, Args... args) { + if (g_insideWrapper == 0 && Enabled()) { + if (auto wrapped = reinterpret_cast(WrapperFunction(name))) { + ++g_insideWrapper; + const HRESULT result = wrapped(args...); + --g_insideWrapper; + return result; + } + } + return original(args...); +} + +} // namespace + +bool IsActive() { + if (!Enabled()) { + return false; + } + if (g_loadAttempted) { + return g_wrapper != nullptr; + } + return GetFileAttributesW(WrapperPath().c_str()) != INVALID_FILE_ATTRIBUTES; +} + +void FixDeviceInfo(void* deviceInfo) { + auto* bytes = static_cast(deviceInfo); + const uint32_t guidAddress = static_cast(reinterpret_cast(bytes + 0x3f4)); + uint32_t guidPointer; + memcpy(&guidPointer, bytes + 4, 4); + if (guidPointer != guidAddress) { + re::Log("dxwrapper: device info GUID pointer was 0x%08x, set to 0x%08x", guidPointer, guidAddress); + memcpy(bytes + 4, &guidAddress, 4); + } +} + +HRESULT WINAPI DirectDrawCreateExViaWrapper(GUID* guid, LPVOID* directDraw, REFIID iid, IUnknown* outer); +HRESULT WINAPI DirectDrawCreateViaWrapper(GUID* guid, LPDIRECTDRAW* directDraw, IUnknown* outer); +HRESULT WINAPI DirectDrawEnumerateExAViaWrapper(LPDDENUMCALLBACKEXA callback, LPVOID context, DWORD flags); + +RE_HOOK_API(DirectDrawCreateEx, DirectDrawCreateExViaWrapper); +RE_HOOK_API(DirectDrawCreate, DirectDrawCreateViaWrapper); +RE_HOOK_API(DirectDrawEnumerateExA, DirectDrawEnumerateExAViaWrapper); + +HRESULT WINAPI DirectDrawCreateExViaWrapper(GUID* guid, LPVOID* directDraw, REFIID iid, IUnknown* outer) { + return Forward("DirectDrawCreateEx", DirectDrawCreateExViaWrapper_original, guid, directDraw, iid, outer); +} + +HRESULT WINAPI DirectDrawCreateViaWrapper(GUID* guid, LPDIRECTDRAW* directDraw, IUnknown* outer) { + return Forward("DirectDrawCreate", DirectDrawCreateViaWrapper_original, guid, directDraw, outer); +} + +HRESULT WINAPI DirectDrawEnumerateExAViaWrapper(LPDDENUMCALLBACKEXA callback, LPVOID context, DWORD flags) { + return Forward("DirectDrawEnumerateExA", DirectDrawEnumerateExAViaWrapper_original, callback, context, flags); +} + +} // namespace mods::dxwrapper diff --git a/src/mods/dxwrapper.h b/src/mods/dxwrapper.h new file mode 100644 index 0000000..fb3defd --- /dev/null +++ b/src/mods/dxwrapper.h @@ -0,0 +1,16 @@ +#pragma once + +namespace mods::dxwrapper { + +// True when the game's DirectDraw calls go to dxwrapper (Direct3D 9), which +// has no 2048x2048 render target limit. +bool IsActive(); + +// The device info the game passes to DxDriver7::SetDisplayMode keeps a pointer +// to its own device GUID at +4 (the GUID itself is at +0x3f4); the game uses +// it for EnumZBufferFormats. Under dxwrapper the game's device enumeration +// leaves garbage there (0x3c0) and dxwrapper crashes dereferencing it. +// Restores the pointer Windows' DirectDraw would have produced. +void FixDeviceInfo(void* deviceInfo); + +} // namespace mods::dxwrapper diff --git a/src/mods/widescreen.cpp b/src/mods/widescreen.cpp new file mode 100644 index 0000000..a714b97 --- /dev/null +++ b/src/mods/widescreen.cpp @@ -0,0 +1,249 @@ +// Widescreen: render the world 1:1 at the monitor's resolution, showing more +// of it, instead of scaling a 1024x768 image. +// +// The world view sizes itself from zoom * 1024.0 / 768.0 (float constants that +// the UI also reads) and projects 3D models with an ortho camera of +// +-267 x +-200 world units (times zoom). We redirect only the world view's +// reads of those constants to our own variables, so the UI keeps its +// 1024x768 layout, and set the variables from the render size. +// See docs/widescreen.md. +// +// sacred_re.ini: +// [widescreen] +// Enabled=1 +// Width=0 ; 0 = monitor width +// Height=0 ; 0 = monitor height +// +// Windows' Direct3D 7 limits the render size to 2048x2048 unless the dxwrapper +// mod (Direct3D 9) is active; the borderless mod centres a smaller image 1:1. +// +// Experimental: the UI is still drawn at the top-left 1024x768, and creatures +// are only active within the original distance. + +#include "mods/widescreen.h" + +#include +#include +#include +#include +#include +#include + +#include "game/addresses.h" +#include "game/ui/ui_manager.h" +#include "mods/dxwrapper.h" +#include "re/config.h" +#include "re/hook.h" +#include "re/log.h" +#include "re/patch.h" + +namespace mods::widescreen { +namespace { + +constexpr float kOriginalWidth = 1024.0f; +constexpr float kOriginalHeight = 768.0f; +constexpr double kOriginalOrthoX = 267.0; +constexpr double kOriginalOrthoY = 200.0; + +// Read by the patched world view instructions; the initial values reproduce +// the original behaviour until a render size is applied. +float g_viewWidth = kOriginalWidth; +float g_viewHeight = kOriginalHeight; +double g_orthoLeft = -kOriginalOrthoX; +double g_orthoRight = kOriginalOrthoX; +double g_orthoBottom = -kOriginalOrthoY; +double g_orthoTop = kOriginalOrthoY; + +bool g_pushesPatched = false; + +// Render size once applied (0 = original 1024x768 behaviour). +int g_renderWidth = 0; +int g_renderHeight = 0; + +// Frames from the main menu thread and the loading screen are laid out for +// 1024x768 ("classic"); everything else is the widened in-game view. +std::atomic g_menuThread = 0; +thread_local int g_loadingScreenDepth = 0; + +// D3DVIEWPORT7 (d3d.h is not in current Windows SDKs). +struct Viewport7 { + DWORD x, y, width, height; + float minZ, maxZ; +}; + +bool Enabled() { + return re::ConfigInt("widescreen", "Enabled", 1) != 0; +} + +// The world view's tile draw list only holds ~1750 entries (enough for +// 1024x768, unchecked). Allocate the view larger and move the list to the new +// space at its end, by rewriting the field displacements of every instruction +// that addresses it. +constexpr uint32_t kTileListCapacity = 16384; + +void RelocateTileList() { + using namespace game::addr; + const uint32_t newOffset = kWorldViewSize; + const uint32_t newSize = kWorldViewSize + kTileListCapacity * kTileListEntrySize; + if (!re::PatchPushImm32(kWorldViewAllocSizePush, kWorldViewSize, newSize)) { + re::Log("widescreen: world view allocation not patched, tile list stays at 1750 entries"); + return; + } + int patched = 0; + for (uint32_t instruction : kTileListUsers) { + // The displacement is kTileListOffset + field; find it in the instruction. + const auto* code = reinterpret_cast(static_cast(instruction)); + for (int offset = 1; offset <= 7; ++offset) { + uint32_t value; + memcpy(&value, code + offset, 4); + if (value >= kTileListOffset && value < kTileListOffset + kTileListEntrySize) { + patched += re::PatchInstructionDword(instruction, value, newOffset + (value - kTileListOffset)); + break; + } + } + } + re::Log("widescreen: tile draw list moved to +0x%x (%u entries), %d/%d instructions patched", newOffset, + kTileListCapacity, patched, static_cast(std::size(kTileListUsers))); +} + +void RedirectWorldView() { + if (!Enabled()) { + return; + } + int patched = 0; + int total = 0; + for (uint32_t instruction : game::addr::kViewWidthUsers) { + patched += re::RedirectOperand(instruction, game::addr::kScreenWidthF, &g_viewWidth); + ++total; + } + for (uint32_t instruction : game::addr::kViewHeightUsers) { + patched += re::RedirectOperand(instruction, game::addr::kScreenHeightF, &g_viewHeight); + ++total; + } + for (const auto& site : game::addr::kOrthoZoomedUsers) { + patched += re::RedirectOperand(site[0], game::addr::kOrthoTopD, &g_orthoTop); + patched += re::RedirectOperand(site[1], game::addr::kOrthoBottomD, &g_orthoBottom); + patched += re::RedirectOperand(site[2], game::addr::kOrthoRightD, &g_orthoRight); + patched += re::RedirectOperand(site[3], game::addr::kOrthoLeftD, &g_orthoLeft); + total += 4; + } + re::Log("widescreen: %d/%d world view operands redirected", patched, total); + RelocateTileList(); +} +RE_STARTUP(WidescreenRedirect, RedirectWorldView); + +uint32_t FloatBits(float value) { + return std::bit_cast(value); +} + +// The unzoomed projection is pushed as immediates; rewrite them once. +void PatchOrthoPushes(float right, float top) { + if (g_pushesPatched) { + return; + } + int patched = 0; + for (uint32_t site : game::addr::kOrthoPushSequences) { + patched += re::PatchPushImm32(site + 0, FloatBits(200.0f), FloatBits(top)); + patched += re::PatchPushImm32(site + 5, FloatBits(-200.0f), FloatBits(-top)); + patched += re::PatchPushImm32(site + 10, FloatBits(267.0f), FloatBits(right)); + patched += re::PatchPushImm32(site + 15, FloatBits(-267.0f), FloatBits(-right)); + } + g_pushesPatched = true; + re::Log("widescreen: %d/%d projection immediates patched", patched, + static_cast(std::size(game::addr::kOrthoPushSequences)) * 4); +} + +} // namespace + +void ApplyRenderSize(game::DxDisplayMode& mode, HWND window) { + if (!Enabled()) { + return; + } + MONITORINFO monitor = {sizeof(monitor)}; + GetMonitorInfoW(MonitorFromWindow(window, MONITOR_DEFAULTTOPRIMARY), &monitor); + int width = re::ConfigInt("widescreen", "Width", 0); + int height = re::ConfigInt("widescreen", "Height", 0); + if (width <= 0) { + width = monitor.rcMonitor.right - monitor.rcMonitor.left; + } + if (height <= 0) { + height = monitor.rcMonitor.bottom - monitor.rcMonitor.top; + } + // Windows' Direct3D 7 runtime cannot create a device for a render target + // wider or taller than 2048 pixels (CreateDevice fails with + // DDERR_INVALIDOBJECT); anything larger needs a different renderer. + constexpr int kMaxD3D7Surface = 2048; + if (!mods::dxwrapper::IsActive() && (width > kMaxD3D7Surface || height > kMaxD3D7Surface)) { + re::Log("widescreen: %dx%d exceeds the Direct3D 7 limit, clamping to %d", width, height, kMaxD3D7Surface); + width = std::min(width, kMaxD3D7Surface); + height = std::min(height, kMaxD3D7Surface); + } + // The game's own layout needs at least its original size. + if (width < kOriginalWidth || height < kOriginalHeight) { + re::Log("widescreen: %dx%d is smaller than 1024x768, keeping the original size", width, height); + return; + } + + mode.width = static_cast(width); + mode.height = static_cast(height); + + g_viewWidth = static_cast(width); + g_viewHeight = static_cast(height); + const double scaleX = width / kOriginalWidth; + const double scaleY = height / kOriginalHeight; + g_orthoRight = kOriginalOrthoX * scaleX; + g_orthoLeft = -g_orthoRight; + g_orthoTop = kOriginalOrthoY * scaleY; + g_orthoBottom = -g_orthoTop; + PatchOrthoPushes(static_cast(g_orthoRight), static_cast(g_orthoTop)); + + g_renderWidth = width; + g_renderHeight = height; + re::Log("widescreen: render size %dx%d, ortho x +-%.1f y +-%.1f", width, height, g_orthoRight, g_orthoTop); +} + +bool IsClassicFrame() { + if (g_renderWidth == 0) { + return false; + } + return g_loadingScreenDepth > 0 || GetCurrentThreadId() == g_menuThread.load(); +} + +} // namespace mods::widescreen + +namespace game { + +using namespace mods::widescreen; + +RE_HOOK(addr::UiManager_ThreadLoop, UiManager_ThreadLoop); +RE_HOOK(addr::DxDriver7_LoadingScreen, DxDriver7_LoadingScreen); +RE_HOOK(addr::DxDriver7_BeginScene, DxDriver7_BeginScene); + +void __fastcall UiManager_ThreadLoop(void* self, void* edx) { + g_menuThread = GetCurrentThreadId(); + UiManager_ThreadLoop_original(self, edx); + g_menuThread = 0; +} + +void __fastcall DxDriver7_LoadingScreen(DxDriver7* self, void* edx, uint32_t progress, const char* text) { + ++g_loadingScreenDepth; + DxDriver7_LoadingScreen_original(self, edx, progress, text); + --g_loadingScreenDepth; +} + +// Menus draw their 3D parts for a 1024x768 screen: give them a 1024x768 +// viewport (the borderless mod then presents just that area, scaled), and the +// world the full render size. +int __fastcall DxDriver7_BeginScene(DxDriver7* self, void* edx) { + if (g_renderWidth != 0 && self->sceneDepth == 0 && self->device) { + const bool classic = IsClassicFrame(); + Viewport7 viewport = {0, 0, classic ? 1024u : static_cast(g_renderWidth), + classic ? 768u : static_cast(g_renderHeight), 0.0f, 1.0f}; + using SetViewportFn = HRESULT(__stdcall*)(void* device, Viewport7* viewport); + auto setViewport = reinterpret_cast((*static_cast(self->device))[13]); + setViewport(self->device, &viewport); + } + return DxDriver7_BeginScene_original(self, edx); +} + +} // namespace game diff --git a/src/mods/widescreen.h b/src/mods/widescreen.h new file mode 100644 index 0000000..15c277d --- /dev/null +++ b/src/mods/widescreen.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +#include "game/render/dx_driver7.h" + +namespace mods::widescreen { + +// Called by the borderless mod before the renderer creates its surfaces: +// sets the render size to the monitor (or the configured size) and resizes +// the world view to match. No-op when disabled. +void ApplyRenderSize(game::DxDisplayMode& mode, HWND window); + +// True while the current thread renders a frame laid out for 1024x768 (main +// menu, loading screen); the borderless mod then presents only that area. +bool IsClassicFrame(); + +} // namespace mods::widescreen diff --git a/src/re/config.cpp b/src/re/config.cpp index fbf0625..23114b7 100644 --- a/src/re/config.cpp +++ b/src/re/config.cpp @@ -23,6 +23,10 @@ const std::wstring& ConfigPath() { return g_path; } +std::wstring ModuleDirectory() { + return g_path.substr(0, g_path.find_last_of(L"\\/") + 1); +} + int ConfigInt(const char* section, const char* key, int fallback) { if (g_path.empty()) { return fallback; diff --git a/src/re/config.h b/src/re/config.h index 5072e29..24eb048 100644 --- a/src/re/config.h +++ b/src/re/config.h @@ -7,6 +7,8 @@ namespace re { void SetConfigPath(const std::wstring& path); const std::wstring& ConfigPath(); +// Folder of sacred_re.dll (with trailing separator), derived from the config path. +std::wstring ModuleDirectory(); int ConfigInt(const char* section, const char* key, int fallback); } // namespace re diff --git a/src/re/dllmain.cpp b/src/re/dllmain.cpp index 18e6248..b8c7f73 100644 --- a/src/re/dllmain.cpp +++ b/src/re/dllmain.cpp @@ -10,6 +10,7 @@ #include "re/config.h" #include "re/hook.h" #include "re/log.h" +#include "re/patch.h" namespace { @@ -58,6 +59,7 @@ void Startup(HMODULE module) { re::Log(IsDebuggerPresent() ? "debugger attached" : "no debugger, continuing"); } re::InstallHooks(); + re::RunStartupFunctions(); } } // namespace diff --git a/src/re/patch.cpp b/src/re/patch.cpp new file mode 100644 index 0000000..4fd64d3 --- /dev/null +++ b/src/re/patch.cpp @@ -0,0 +1,81 @@ +#include "re/patch.h" + +#include +#include + +#include + +#include "re/log.h" + +namespace re { +namespace { + +struct Startup { + const char* name; + void (*function)(); +}; + +std::vector& Startups() { + static std::vector startups; + return startups; +} + +// Longest x86 instruction is 15 bytes; an absolute operand starts within it. +constexpr size_t kOperandSearch = 11; + +} // namespace + +bool WriteCode(uint32_t address, const void* bytes, size_t size) { + void* target = reinterpret_cast(static_cast(address)); + DWORD oldProtect; + if (!VirtualProtect(target, size, PAGE_EXECUTE_READWRITE, &oldProtect)) { + Log("patch 0x%08x: VirtualProtect failed (%lu)", address, GetLastError()); + return false; + } + std::memcpy(target, bytes, size); + VirtualProtect(target, size, oldProtect, &oldProtect); + FlushInstructionCache(GetCurrentProcess(), target, size); + return true; +} + +bool PatchInstructionDword(uint32_t instruction, uint32_t from, uint32_t to) { + const auto* code = reinterpret_cast(static_cast(instruction)); + for (size_t offset = 1; offset + 4 <= kOperandSearch; ++offset) { + uint32_t operand; + std::memcpy(&operand, code + offset, 4); + if (operand == from) { + return WriteCode(instruction + static_cast(offset), &to, 4); + } + } + Log("patch 0x%08x: operand 0x%08x not found, skipped", instruction, from); + return false; +} + +bool RedirectOperand(uint32_t instruction, uint32_t from, const void* to) { + return PatchInstructionDword(instruction, from, static_cast(reinterpret_cast(to))); +} + +bool PatchPushImm32(uint32_t instruction, uint32_t expected, uint32_t value) { + const auto* code = reinterpret_cast(static_cast(instruction)); + uint32_t current; + std::memcpy(¤t, code + 1, 4); + if (code[0] != 0x68 || current != expected) { + Log("patch 0x%08x: expected push 0x%08x, found %02x %08x, skipped", instruction, expected, code[0], + current); + return false; + } + return WriteCode(instruction + 1, &value, 4); +} + +StartupRegistration::StartupRegistration(const char* name, void (*function)()) { + Startups().push_back({name, function}); +} + +void RunStartupFunctions() { + for (const Startup& startup : Startups()) { + Log("startup: %s", startup.name); + startup.function(); + } +} + +} // namespace re diff --git a/src/re/patch.h b/src/re/patch.h new file mode 100644 index 0000000..003be52 --- /dev/null +++ b/src/re/patch.h @@ -0,0 +1,33 @@ +#pragma once +// Verified in-place patches of sacred.exe code. Every function checks that the +// bytes it expects are present before writing, logs and refuses otherwise. + +#include +#include + +namespace re { + +bool WriteCode(uint32_t address, const void* bytes, size_t size); + +// The instruction at `instruction` has the absolute memory operand `from` +// (e.g. `fmul st, dword ptr [from]`); make it read `to` instead. +bool RedirectOperand(uint32_t instruction, uint32_t from, const void* to); + +// Replace a 32-bit displacement/immediate `from` inside the instruction at +// `instruction` by `to` (e.g. a struct field offset in `[ebp+ecx*8+3FF2Ch]`). +bool PatchInstructionDword(uint32_t instruction, uint32_t from, uint32_t to); + +// `push imm32` (68 xx xx xx xx) at `instruction`: replace `expected` by `value`. +bool PatchPushImm32(uint32_t instruction, uint32_t expected, uint32_t value); + +// Code that must run once at startup, after the hooks are installed. +class StartupRegistration { +public: + StartupRegistration(const char* name, void (*function)()); +}; +void RunStartupFunctions(); + +} // namespace re + +#define RE_STARTUP(name, function) \ + static ::re::StartupRegistration name##_startup(#name, &function)