Widescreen: keep original size, extend view horizontally; fix player/camera
- Default render size is 768 high and as wide as the monitor aspect
(1835x768 on 3440x1440), scaled up: everything keeps the size players
know and the view only grows sideways. Height=0 keeps native 1:1.
- Ground: 818/888 clip limits, screen centre floats and integer
immediates, ground quad list (capped at 1749) moved with the tile list.
- 3D: cGranny ortho (+-512 x +-384, incl. HUD items) scaled; inverse
projection 0x623a20 (1/1024, 1/768) follows the render size, which fixes
the camera following the player ("outside world") and the missing
player / ESC menu.
- tools: game-input.ps1 + autotest-load.ps1 drive the game (load the first
save game, screenshots in-game and with the ESC menu).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
795fe8f087
commit
df31c9c105
6 files changed
+324
-45
No files matched your search
+127
-44
@@ -11,8 +11,9 @@
|
||||
// sacred_re.ini:
|
||||
// [widescreen]
|
||||
// Enabled=1
|
||||
// Width=0 ; 0 = monitor width
|
||||
// Height=0 ; 0 = monitor height
|
||||
// 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 limits the render size to 2048x2048 unless the dxwrapper
|
||||
// mod (Direct3D 9) is active; the borderless mod centres a smaller image 1:1.
|
||||
@@ -75,63 +76,115 @@ 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;
|
||||
// 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;
|
||||
|
||||
void RelocateTileList() {
|
||||
// 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<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 >= 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 newOffset = kWorldViewSize;
|
||||
const uint32_t newSize = kWorldViewSize + kTileListCapacity * kTileListEntrySize;
|
||||
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, tile list stays at 1750 entries");
|
||||
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) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
patched += MoveDisplacement(instruction, kTileListOffset, kTileListEntrySize, tileList);
|
||||
++total;
|
||||
}
|
||||
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)));
|
||||
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 <size_t N>
|
||||
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;
|
||||
}
|
||||
int patched = 0;
|
||||
using namespace game::addr;
|
||||
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);
|
||||
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);
|
||||
RelocateTileList();
|
||||
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<uint32_t>(width / 2));
|
||||
patched += re::PatchInstructionDword(pair[1], 0x180, static_cast<uint32_t>(height / 2));
|
||||
}
|
||||
re::Log("widescreen: %d/%d screen centre immediates patched", patched,
|
||||
static_cast<int>(std::size(game::addr::kCenterImmediates)) * 2);
|
||||
}
|
||||
|
||||
uint32_t FloatBits(float value) {
|
||||
return std::bit_cast<uint32_t>(value);
|
||||
}
|
||||
@@ -153,6 +206,21 @@ void PatchOrthoPushes(float right, float top) {
|
||||
static_cast<int>(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<int>(std::size(game::addr::kGrannyOrthoPushSequences)) * 4);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ApplyRenderSize(game::DxDisplayMode& mode, HWND window) {
|
||||
@@ -161,13 +229,18 @@ void ApplyRenderSize(game::DxDisplayMode& mode, HWND window) {
|
||||
}
|
||||
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;
|
||||
}
|
||||
// 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<int>(kOriginalHeight));
|
||||
if (height <= 0) {
|
||||
height = monitor.rcMonitor.bottom - monitor.rcMonitor.top;
|
||||
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
|
||||
@@ -189,6 +262,16 @@ void ApplyRenderSize(game::DxDisplayMode& mode, HWND window) {
|
||||
|
||||
g_viewWidth = static_cast<float>(width);
|
||||
g_viewHeight = static_cast<float>(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);
|
||||
PatchGrannyOrthoPushes(width, height);
|
||||
}
|
||||
const double scaleX = width / kOriginalWidth;
|
||||
const double scaleY = height / kOriginalHeight;
|
||||
g_orthoRight = kOriginalOrthoX * scaleX;
|
||||
|
||||
Reference in new issue
Block a user