// Widescreen: show more of the world horizontally on wide monitors while // everything keeps its original on-screen size. // // 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 // Height=768 ; world pixels visible vertically (768 = original look); // ; 0 = monitor height, i.e. native 1:1 (everything smaller) // Width=0 ; 0 = Height * monitor aspect ratio // // Windows' Direct3D 7 cannot render to targets larger than 2048x2048, so the // render size is clamped to that (only reachable with Height=0 / large Width). // // 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 "mods/hud.h" #include #include #include #include #include #include #include "game/addresses.h" #include "game/ui/ui_manager.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; // Whether the most recent scene (on any thread) was a classic one: the main // menu is showing. UI input is processed outside the menu thread. std::atomic g_lastSceneClassic = false; 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 keeps two fixed-size per-frame lists sized for 1024x768: // the tile draw list (~1750 entries, unchecked) and the ground quad list // (1749, capped: the ground just stops). Allocate the view larger and move // both lists into the new space at its end by rewriting the field // displacements of every instruction that addresses them. constexpr uint32_t kListCapacity = 16384; // Rewrites the displacement `oldBase + field` in `instruction` to // `newBase + field`, for any field in [0, fieldRange). bool MoveDisplacement(uint32_t instruction, uint32_t oldBase, uint32_t fieldRange, uint32_t newBase) { 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 >= oldBase && value < oldBase + fieldRange) { return re::PatchInstructionDword(instruction, value, newBase + (value - oldBase)); } } re::Log("widescreen: no displacement 0x%x.. in instruction 0x%08x", oldBase, instruction); return false; } void RelocateViewLists() { using namespace game::addr; const uint32_t tileList = kWorldViewSize; const uint32_t groundQuads = tileList + kListCapacity * kTileListEntrySize; const uint32_t groundIndices = groundQuads + kListCapacity * kGroundQuadEntrySize; const uint32_t newSize = groundIndices + kListCapacity * 4; if (!re::PatchPushImm32(kWorldViewAllocSizePush, kWorldViewSize, newSize)) { re::Log("widescreen: world view allocation not patched, lists keep their 1024x768 capacity"); return; } int patched = 0; int total = 0; for (uint32_t instruction : kTileListUsers) { patched += MoveDisplacement(instruction, kTileListOffset, kTileListEntrySize, tileList); ++total; } for (uint32_t instruction : kGroundQuadUsers) { patched += MoveDisplacement(instruction, kGroundQuadOffset, kGroundQuadEntrySize, groundQuads); ++total; } for (uint32_t instruction : kGroundQuadIndexUsers) { patched += MoveDisplacement(instruction, kGroundQuadIndexOffset, 4, groundIndices); ++total; } patched += re::PatchInstructionDword(kGroundQuadCapCompare, kGroundQuadCap, kListCapacity - 1); ++total; re::Log("widescreen: view lists moved (%u entries each, view object 0x%x bytes), %d/%d instructions patched", kListCapacity, newSize, patched, total); } // Screen size-dependent floats read by the patched instructions. float g_viewHalfWidth = kOriginalWidth / 2; float g_viewHalfHeight = kOriginalHeight / 2; float g_groundClipBottom = kOriginalHeight + 50; float g_clipBottom888 = kOriginalHeight + 120; float g_viewInvWidth = 1.0f / kOriginalWidth; float g_viewInvHeight = 1.0f / kOriginalHeight; template int Redirect(const uint32_t (&instructions)[N], uint32_t from, const void* to, int& total) { int patched = 0; for (uint32_t instruction : instructions) { patched += re::RedirectOperand(instruction, from, to); ++total; } return patched; } void RedirectWorldView() { if (!Enabled()) { return; } using namespace game::addr; int total = 0; int patched = Redirect(kViewWidthUsers, kScreenWidthF, &g_viewWidth, total); patched += Redirect(kViewHeightUsers, kScreenHeightF, &g_viewHeight, total); patched += Redirect(kViewHalfWidthUsers, kScreenHalfWidthF, &g_viewHalfWidth, total); patched += Redirect(kViewHalfHeightUsers, kScreenHalfHeightF, &g_viewHalfHeight, total); patched += Redirect(kCreatureHalfWidthUsers, kScreenHalfWidthF, &g_viewHalfWidth, total); patched += Redirect(kCreatureHalfHeightUsers, kScreenHalfHeightF, &g_viewHalfHeight, total); patched += Redirect(kViewInvWidthUsers, kScreenInvWidthF, &g_viewInvWidth, total); patched += Redirect(kViewInvHeightUsers, kScreenInvHeightF, &g_viewInvHeight, total); patched += Redirect(kGroundClipBottomUsers, kGroundClipBottomF, &g_groundClipBottom, total); patched += Redirect(kClipBottom888Users, kClipBottom888F, &g_clipBottom888, total); for (const auto& site : kOrthoZoomedUsers) { patched += re::RedirectOperand(site[0], kOrthoTopD, &g_orthoTop); patched += re::RedirectOperand(site[1], kOrthoBottomD, &g_orthoBottom); patched += re::RedirectOperand(site[2], kOrthoRightD, &g_orthoRight); patched += re::RedirectOperand(site[3], kOrthoLeftD, &g_orthoLeft); total += 4; } re::Log("widescreen: %d/%d world view operands redirected", patched, total); RelocateViewLists(); } RE_STARTUP(WidescreenRedirect, RedirectWorldView); // The screen centre also appears as integer immediates (512 / 384). void PatchCenterImmediates(int width, int height) { int patched = 0; for (const auto& pair : game::addr::kCenterImmediates) { patched += re::PatchInstructionDword(pair[0], 0x200, static_cast(width / 2)); patched += re::PatchInstructionDword(pair[1], 0x180, static_cast(height / 2)); } for (const auto& pair : game::addr::kNegativeCenterImmediates) { patched += re::PatchInstructionDword(pair[0], static_cast(-0x200), static_cast(-(width / 2))); patched += re::PatchInstructionDword(pair[1], static_cast(-0x180), static_cast(-(height / 2))); } re::Log("widescreen: %d/%d screen centre immediates patched", patched, static_cast(std::size(game::addr::kCenterImmediates) + std::size(game::addr::kNegativeCenterImmediates)) * 2); } // Creatures beyond the original screen (plus a margin) are not drawn. void PatchCreatureClip(int width, int height) { using namespace game::addr; int patched = 0; for (uint32_t site : kCreatureClipRight) { patched += re::PatchInstructionDword(site, 1024 + kCreatureClipMargin, width + kCreatureClipMargin); } for (uint32_t site : kCreatureClipBottom) { patched += re::PatchInstructionDword(site, 768 + kCreatureClipMargin, height + kCreatureClipMargin); } re::Log("widescreen: %d/%d creature clip immediates patched", patched, static_cast(std::size(kCreatureClipRight) + std::size(kCreatureClipBottom))); } 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); } // 3D creatures (cGranny) are projected 1:1 in screen pixels around the centre. void PatchGrannyOrthoPushes(int width, int height) { const float right = width / 2.0f; const float top = height / 2.0f; int patched = 0; for (uint32_t site : game::addr::kGrannyOrthoPushSequences) { patched += re::PatchPushImm32(site + 0, FloatBits(384.0f), FloatBits(top)); patched += re::PatchPushImm32(site + 5, FloatBits(-384.0f), FloatBits(-top)); patched += re::PatchPushImm32(site + 10, FloatBits(512.0f), FloatBits(right)); patched += re::PatchPushImm32(site + 15, FloatBits(-512.0f), FloatBits(-right)); } re::Log("widescreen: %d/%d creature projection immediates patched", patched, static_cast(std::size(game::addr::kGrannyOrthoPushSequences)) * 4); } // Labels are laid out within the screen; widen their area. void PatchLabelArea(int width, int height) { int patched = 0; for (uint32_t site : game::addr::kLabelAreaPushes) { patched += re::PatchPushImm32(site + 0, 0x300, static_cast(height)); patched += re::PatchPushImm32(site + 5, 0x400, static_cast(width)); } re::Log("widescreen: %d/%d label area immediates patched", patched, static_cast(std::size(game::addr::kLabelAreaPushes)) * 2); } // Cinematic bars span the screen width. void PatchLetterbox(int width) { int patched = 0; for (uint32_t site : game::addr::kLetterboxRightImmediates) { patched += re::PatchInstructionDword(site, FloatBits(1024.0f), FloatBits(static_cast(width))); } re::Log("widescreen: %d/%d letterbox immediates patched", patched, static_cast(std::size(game::addr::kLetterboxRightImmediates))); } } // namespace void ApplyRenderSize(game::DxDisplayMode& mode, HWND window) { if (!Enabled()) { return; } MONITORINFO monitor = {sizeof(monitor)}; GetMonitorInfoW(MonitorFromWindow(window, MONITOR_DEFAULTTOPRIMARY), &monitor); // Default: keep the original 768 pixels of world height (so everything // keeps its on-screen size once scaled to the monitor) and extend the width // to the monitor's aspect ratio. Height=0 renders at the monitor height 1:1. const int monitorWidth = monitor.rcMonitor.right - monitor.rcMonitor.left; const int monitorHeight = monitor.rcMonitor.bottom - monitor.rcMonitor.top; int height = re::ConfigInt("widescreen", "Height", static_cast(kOriginalHeight)); if (height <= 0) { height = monitorHeight; } int width = re::ConfigInt("widescreen", "Width", 0); if (width <= 0) { width = MulDiv(height, monitorWidth, monitorHeight); } // 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 (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); g_viewHalfWidth = width / 2.0f; g_viewHalfHeight = height / 2.0f; g_groundClipBottom = height + 50.0f; g_clipBottom888 = height + 120.0f; g_viewInvWidth = 1.0f / width; g_viewInvHeight = 1.0f / height; if (!g_pushesPatched) { PatchCenterImmediates(width, height); PatchCreatureClip(width, height); PatchGrannyOrthoPushes(width, height); PatchLabelArea(width, height); PatchLetterbox(width); } 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); } int RenderWidth() { return g_renderWidth; } bool MenuIsShowing() { return g_lastSceneClassic; } 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) { mods::hud::OnDevice(self->device); if (self->sceneDepth == 0) { g_lastSceneClassic = IsClassicFrame(); } 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