widescreen: draw creatures across the whole view; HUD: copy the cursor per thread

- Creatures were culled beyond 1024+200 x 768+200 screen pixels (cmp 4C8h /
  3C8h in both object passes): invisible in the right part of a wide view,
  and the player too once the centre passed 1224. The bounds follow the
  render size now.
- Centre immediates written as negatives (-200h / -180h), missed before:
  the row renderer's static object origin (0x62babb) and aiming at the
  cursor (0x4fb620).
- HUD: moved windows no longer shift the shared mouse cursor object; the
  cursor accessor (0x6550f0) hands them a per-thread shifted copy and writes
  back what they changed. World input on other threads read the shifted
  position now and then, which made hold-to-move jerky.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-14 08:16:27 +02:00
1 parent ba03672c32
commit d39f12ab4b
6 files changed
+145 -33

No files matched your search

+15
View File
@@ -84,6 +84,21 @@ constexpr uint32_t kCenterImmediates[][2] = {
// floating texts (damage numbers): world -> screen, and back (sub)
{0x0041e560, 0x0041e596}, {0x0041eb07, 0x0041eaf7},
};
// The same, negated (`add reg, -200h` / `lea reg, [...-200h]`), pairs x, y.
constexpr uint32_t kNegativeCenterImmediates[][2] = {
// aiming at the cursor (0x4fb620): cursor - centre + camera -> world
{0x004fb6ef, 0x004fb6f9},
// row renderer 0x62b000: screen origin (camera - centre) for the static
// objects it puts into the per-view draw lists
{0x0062babb, 0x0062bac1},
};
// Creatures are drawn only while their screen position lies within
// -200 .. 1024+200 x -200 .. 768+200 (`cmp reg, 4C8h` / `cmp eax, 3C8h`, in
// the two object passes 0x62e580 and 0x6300d0); outside it they only update.
constexpr uint32_t kCreatureClipRight[] = {0x0062f988, 0x006315cb}; // 0x4c8
constexpr uint32_t kCreatureClipBottom[] = {0x0062f9a7, 0x006315ea}; // 0x3c8
constexpr uint32_t kCreatureClipMargin = 200;
// cWorldView0 is allocated with `push 970F4h; call operator new` here.
constexpr uint32_t kWorldViewAllocSizePush = 0x006101aa;
+86 -28
View File
@@ -55,15 +55,42 @@ int OffsetFor(Anchor anchor) {
return anchor == Anchor::Center ? (width - kLayoutWidth) / 2 : width - kLayoutWidth;
}
// Offset by which the current thread has shifted the mouse cursor (0: none).
// Offset by which the current thread sees the mouse cursor shifted (0: none).
thread_local int g_cursorDx = 0;
// RAII: shift the mouse cursor object's x by -dx for the scope (outermost
// window only). Windows poll it for hover (highlights, tooltips) while they
// render and handle events: +4 is the current x, +0xc the one before the last
// update (read by 0x6559a0, e.g. when an inventory item is picked up). The
// window thread may store a new position meanwhile; that one is kept instead
// of restoring.
// The mouse cursor object (0x70 bytes, the only instance is returned by
// MouseCursor_Instance 0x6550f0): x, y at +4 / +8, the position before the
// last update at +0xc / +0x10 (read by 0x6559a0, e.g. when an inventory item
// is picked up), a CRITICAL_SECTION at +0x24 guarding the carried item.
constexpr size_t kCursorSize = 0x70;
constexpr size_t kCursorLockOffset = 0x24;
constexpr size_t kCursorLockEnd = kCursorLockOffset + sizeof(CRITICAL_SECTION);
static_assert(kCursorLockEnd == 0x3c);
// Per-thread copy of the cursor object, x shifted by -dx, that the accessor
// returns while a moved window runs on this thread. The shared object is never
// shifted: other threads (world picking, hold-to-move, the cursor sprite) read
// it at any time.
struct CursorShadow {
alignas(8) uint8_t bytes[kCursorSize];
uint8_t snapshot[kCursorSize]; // bytes as handed out, to find what the window changed
uint8_t* real = nullptr;
bool lockInitialized = false;
};
thread_local CursorShadow g_cursorShadow;
CRITICAL_SECTION* CursorLock(uint8_t* cursor) {
return reinterpret_cast<CRITICAL_SECTION*>(cursor + kCursorLockOffset);
}
int32_t& CursorField(uint8_t* cursor, size_t offset) {
return *reinterpret_cast<int32_t*>(cursor + offset);
}
// RAII: windows poll the cursor for hover (highlights, tooltips) while they
// render and handle events; let them see it shifted by -dx for the scope
// (outermost window only). Whatever the window stores into the cursor (carried
// item, a new position) is written back to the shared object afterwards.
class CursorOffset {
public:
explicit CursorOffset(int dx) {
@@ -71,33 +98,52 @@ public:
if (dx == 0 || g_cursorDx != 0 || !cursor) {
return;
}
fields_[0].Shift(cursor + 0x4, dx);
fields_[1].Shift(cursor + 0xc, dx);
CursorShadow& shadow = g_cursorShadow;
if (!shadow.lockInitialized) {
InitializeCriticalSection(CursorLock(shadow.bytes));
shadow.lockInitialized = true;
}
EnterCriticalSection(CursorLock(cursor));
memcpy(shadow.bytes, cursor, kCursorLockOffset);
memcpy(shadow.bytes + kCursorLockEnd, cursor + kCursorLockEnd, kCursorSize - kCursorLockEnd);
LeaveCriticalSection(CursorLock(cursor));
CursorField(shadow.bytes, 0x4) -= dx;
CursorField(shadow.bytes, 0xc) -= dx;
memcpy(shadow.snapshot, shadow.bytes, kCursorSize);
shadow.real = cursor;
g_cursorDx = dx;
active_ = true;
}
~CursorOffset() {
if (g_cursorDx != 0 && fields_[0].x) {
fields_[0].Restore();
fields_[1].Restore();
g_cursorDx = 0;
if (!active_) {
return;
}
CursorShadow& shadow = g_cursorShadow;
// Only dwords the window changed, so concurrent updates of the others
// (e.g. the position from the window thread) are kept.
EnterCriticalSection(CursorLock(shadow.real));
for (size_t offset = 0; offset < kCursorSize; offset += 4) {
if (offset >= kCursorLockOffset && offset < kCursorLockEnd) {
continue;
}
int32_t value = CursorField(shadow.bytes, offset);
if (value == CursorField(shadow.snapshot, offset)) {
continue;
}
if (offset == 0x4 || offset == 0xc) {
value += g_cursorDx;
}
CursorField(shadow.real, offset) = value;
}
LeaveCriticalSection(CursorLock(shadow.real));
shadow.real = nullptr;
g_cursorDx = 0;
}
CursorOffset(const CursorOffset&) = delete;
CursorOffset& operator=(const CursorOffset&) = delete;
private:
struct Field {
volatile LONG* x = nullptr;
LONG original = 0;
LONG shifted = 0;
void Shift(uint8_t* address, int dx) {
x = reinterpret_cast<volatile LONG*>(address);
original = *x;
shifted = original - dx;
*x = shifted;
}
void Restore() { InterlockedCompareExchange(x, original, shifted); }
};
Field fields_[2];
bool active_ = false;
};
// Whether the current thread is inside a hooked window's render method.
@@ -316,6 +362,10 @@ int CursorOffsetX() {
return g_cursorDx;
}
void* CursorFor(void* cursor) {
return g_cursorDx != 0 && cursor == g_cursorShadow.real ? g_cursorShadow.bytes : cursor;
}
void OnDevice(void* device) {
static void* hooked = nullptr;
if (!device || device == hooked) {
@@ -488,7 +538,15 @@ int __fastcall MouseCursor_DrawItem(void* self, void* edx, int a, int b);
RE_HOOK(0x006555e0, MouseCursor_DrawItem);
int __fastcall MouseCursor_DrawItem(void* self, void* edx, int a, int b) {
mods::hud::DrawOffset offset(nullptr, mods::hud::OffsetFor(Anchor::Center));
return MouseCursor_DrawItem_original(self, edx, a, b);
return MouseCursor_DrawItem_original(mods::hud::CursorFor(self), edx, a, b);
}
// Every use of the mouse cursor object goes through this accessor (it creates
// the object on first use); inside a moved window, hand out the shifted copy.
void* __cdecl MouseCursor_Instance();
RE_HOOK(0x006550f0, MouseCursor_Instance);
void* __cdecl MouseCursor_Instance() {
return mods::hud::CursorFor(MouseCursor_Instance_original());
}
// UI manager overlays laid out around x = 512 (0x75bbf0 status/progress line,
+4
View File
@@ -9,4 +9,8 @@ void OnDevice(void* device);
// the current thread (0 outside): polled mouse positions are reduced by it.
int CursorOffsetX();
// The mouse cursor object as the current thread should see it: inside a moved
// window, a private copy with x shifted by CursorOffsetX().
void* CursorFor(void* cursor);
} // namespace mods::hud
+21 -1
View File
@@ -185,8 +185,27 @@ void PatchCenterImmediates(int width, int height) {
patched += re::PatchInstructionDword(pair[0], 0x200, static_cast<uint32_t>(width / 2));
patched += re::PatchInstructionDword(pair[1], 0x180, static_cast<uint32_t>(height / 2));
}
for (const auto& pair : game::addr::kNegativeCenterImmediates) {
patched += re::PatchInstructionDword(pair[0], static_cast<uint32_t>(-0x200), static_cast<uint32_t>(-(width / 2)));
patched += re::PatchInstructionDword(pair[1], static_cast<uint32_t>(-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);
static_cast<int>(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<int>(std::size(kCreatureClipRight) + std::size(kCreatureClipBottom)));
}
uint32_t FloatBits(float value) {
@@ -295,6 +314,7 @@ void ApplyRenderSize(game::DxDisplayMode& mode, HWND window) {
g_viewInvHeight = 1.0f / height;
if (!g_pushesPatched) {
PatchCenterImmediates(width, height);
PatchCreatureClip(width, height);
PatchGrannyOrthoPushes(width, height);
PatchLabelArea(width, height);
PatchLetterbox(width);