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
+11
-1
@@ -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).
|
||||
+8
-3
@@ -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)?
|
||||
@@ -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
@@ -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