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,
|
||||
|
||||
Reference in new issue
Block a user