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:
1 parent
ba03672c32
commit
d39f12ab4b
6 files changed
+145
-33
No files matched your search
+86
-28
@@ -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,
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user