diff --git a/docs/hud.md b/docs/hud.md index 690d427..70534a9 100644 --- a/docs/hud.md +++ b/docs/hud.md @@ -18,7 +18,7 @@ shared base methods are inside the scope): | screen-space vertices (`XYZRHW`) | `IDirect3DDevice7::DrawPrimitive` / `DrawIndexedPrimitive` (vtable 25 / 26) | x + dx | | 3D items (belt, equipment) | ortho projection centred on the screen | none needed | | mouse events | `cEvent_mouseMove` x at +4 (previous) and +0xc; button down / up x at +8 | x - dx, restored afterwards | -| mouse cursor object (`*0xcdbadc`) | x at +4, previous x at +0xc (polled for hover, item pick-up `0x6559a0`) | x - dx, restored unless the window thread stored a new position | +| mouse cursor object (`*0xcdbadc`, accessor `0x6550f0`) | x at +4, previous x at +0xc (polled for hover, item pick-up `0x6559a0`) | the accessor returns a per-thread copy with x - dx; dwords the window changed are written back afterwards | | `GetMouseClientPos` (`0x66e500`) | polled position | x - dx | ## Calls outside a window's scope @@ -42,3 +42,13 @@ shared base methods are inside the scope): mouse events carry positions; category 5 events with subtype 0x10 test a point at +0x48 in `cUI_Window2::vf10`, but commands such as "open ESC menu" (0x11) keep unrelated data there. + +## Why the cursor is copied, not shifted + +UI windows render on the render thread while world input (picking, +hold-to-move, `0x611f10`) reads the same cursor object on another thread. +Shifting the shared object for the duration of a window's scope let the world +sample a position ~400 px off now and then (jerky hold-to-move). Every user of +the cursor goes through the accessor `0x6550f0`, so inside a moved window it +returns a thread-local copy instead (with its own CRITICAL_SECTION at +0x24; +copying and writing back hold the real object's lock). diff --git a/docs/widescreen.md b/docs/widescreen.md index 89636d2..a9c9309 100644 --- a/docs/widescreen.md +++ b/docs/widescreen.md @@ -111,6 +111,14 @@ only grows horizontally. `[widescreen] Height=0` renders natively 1:1 an enlarged view object - 3D ortho extents: world (+-267 x +-200, zoomed and unzoomed) and cGranny (+-512 x +-384, incl. cUI_NetGranny for HUD items) +- creature clip test in both object passes (`0x62f988`, `0x6315cb`): + creatures are drawn only within -200 .. 1024+200 x -200 .. 768+200 screen + pixels (`cmp 4C8h` / `cmp 3C8h`); unpatched, creatures in the right part of + a wide view were invisible, and above ~2450 px width (centre > 1224) even + the player +- centre immediates written as negatives (`add reg, 0FFFFFE00h`, + `lea [..-200h]`), missed by a search for 200h: the row renderer's static + object origin (`0x62babb`) and aiming at the cursor (`0x4fb6ef`) - the inverse projection `0x623a20` (screen -> view) uses 1/1024 and 1/768: game logic (camera follow `0x44d9f0`) converts world -> screen -> view with the device's current projection; unpatched it sent the camera off-world @@ -146,8 +154,5 @@ sector in every direction. The widened view needs at most +-1835 x +-768 px ## Open questions -- Above ~2300 px render width (between 2304 and 2816) the player is not drawn - although monsters are (`cCreature::vf14` still called). Not hit at the - default size; some per-view limit of ~40 tile columns, not found yet. - Fixed-size per-view buffers (e.g. `+0x80e40`, 5 vectors at `+0x96ad0` with capacity 0x200) - do they overflow at 3440x1440 (47 columns x 54 rows)? diff --git a/src/game/addresses.h b/src/game/addresses.h index bbb75e5..7189187 100644 --- a/src/game/addresses.h +++ b/src/game/addresses.h @@ -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; diff --git a/src/mods/hud.cpp b/src/mods/hud.cpp index fdee7f0..4a3b86b 100644 --- a/src/mods/hud.cpp +++ b/src/mods/hud.cpp @@ -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(cursor + kCursorLockOffset); +} + +int32_t& CursorField(uint8_t* cursor, size_t offset) { + return *reinterpret_cast(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(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, diff --git a/src/mods/hud.h b/src/mods/hud.h index 65a1eec..cdff196 100644 --- a/src/mods/hud.h +++ b/src/mods/hud.h @@ -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 diff --git a/src/mods/widescreen.cpp b/src/mods/widescreen.cpp index a0e82c4..8083013 100644 --- a/src/mods/widescreen.cpp +++ b/src/mods/widescreen.cpp @@ -185,8 +185,27 @@ void PatchCenterImmediates(int width, int height) { patched += re::PatchInstructionDword(pair[0], 0x200, static_cast(width / 2)); patched += re::PatchInstructionDword(pair[1], 0x180, static_cast(height / 2)); } + for (const auto& pair : game::addr::kNegativeCenterImmediates) { + patched += re::PatchInstructionDword(pair[0], static_cast(-0x200), static_cast(-(width / 2))); + patched += re::PatchInstructionDword(pair[1], static_cast(-0x180), static_cast(-(height / 2))); + } re::Log("widescreen: %d/%d screen centre immediates patched", patched, - static_cast(std::size(game::addr::kCenterImmediates)) * 2); + static_cast(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(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);