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) <noreply@anthropic.com>
This commit is contained in:
1 parent
c8dc689bc6
commit
795fe8f087
19 files changed
+860
-28
No files matched your search
@@ -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 <algorithm>
|
||||
#include <atomic>
|
||||
#include <bit>
|
||||
#include <iterator>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#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<DWORD> 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<const uint8_t*>(static_cast<uintptr_t>(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<int>(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<uint32_t>(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<int>(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<uint16_t>(width);
|
||||
mode.height = static_cast<uint16_t>(height);
|
||||
|
||||
g_viewWidth = static_cast<float>(width);
|
||||
g_viewHeight = static_cast<float>(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<float>(g_orthoRight), static_cast<float>(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<DWORD>(g_renderWidth),
|
||||
classic ? 768u : static_cast<DWORD>(g_renderHeight), 0.0f, 1.0f};
|
||||
using SetViewportFn = HRESULT(__stdcall*)(void* device, Viewport7* viewport);
|
||||
auto setViewport = reinterpret_cast<SetViewportFn>((*static_cast<void***>(self->device))[13]);
|
||||
setViewport(self->device, &viewport);
|
||||
}
|
||||
return DxDriver7_BeginScene_original(self, edx);
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
Reference in new issue
Block a user