DS2Revival/mods/widescreen/display.cpp

849 lines
41 KiB
C++

// Display: borderless window at the desktop resolution (ultrawide included), menus scaled to fit.
//
// DS2 renders any resolution in windowed mode (the 3D view widens; the HUD is laid out per corner), but:
// - exclusive fullscreen offers few modes, falls back to 640x480 and breaks with overlays (a failed
// IDirect3DDevice9::Reset is fatal: Rapi.cpp "GetBackBuffer" error);
// - the front end (menus) is an 800x600 layout: in a bigger window it stays 800x600 in the top-left.
// So, for [Display] Mode=borderless:
// - the command line gets "fullscreen=false width=W height=H" (desktop size unless Resolution=WxH);
// - the game window becomes a borderless popup covering its monitor;
// - the game is shown a "virtual" client size: whatever it asked for when creating/sizing its window
// (800x600 in the menus, W x H in the world), placed UNSCALED at the top-left corner of the area it is
// shown in. The game measures itself in screen coordinates (sub_5f36cb: GetClientRect + ClientToScreen ->
// renderer size), so its whole "screen" is virtual: ClientToScreen/ScreenToClient/GetWindowRect are plain
// offsets, and only the boundary to the real world scales: GetCursorPos/SetCursorPos, mouse messages;
// - the game's three IDirect3DDevice9::Present calls are patched to present into a centered,
// aspect-correct rectangle of the real window (black bars around it). In the world the virtual size
// equals the window, so nothing is scaled.
// Mode=window only forces windowed rendering at the chosen resolution; Mode=off leaves video alone (except the pointer:
// [Display] ExclusiveCursor, see common/exclusive_cursor.h).
//
// Menu scaling ([Display] MenuScale): the driver's Present stretch filters linearly, which at 2.4x (800x600 on
// a 1440p monitor) blurs the menu art. With MenuScale=sharp (default) or integer, a device the game creates
// smaller than the monitor gets a monitor-sized back buffer instead; the game still draws its 800x600 in the
// top-left corner (it takes its size from the window, not the back buffer), and our Present scales that corner
// itself: sharp = nearest-neighbour up to the next whole multiple, then linear down to the fitted rectangle
// (hard pixel edges, even pixel sizes); integer = the largest whole multiple that fits, nearest-neighbour only
// (smaller, pixel-exact). MenuScale=smooth is the driver's stretch.
#include "../common/d3dhub.h"
#include "../common/exclusive_cursor.h"
#include "../common/gfxcfg.h"
#include "../common/patch.h"
#include "../common/vtable_hook.h"
#include <d3d9.h>
#include <atomic>
#include <cmath>
#include <cstdio>
#include <cstring>
namespace widescreen::hud {
bool Wanted(const ds2::Ini& ini);
bool Install();
void DeviceCreated(IDirect3DDevice9* dev);
void BeforeReset();
void BeforePresent(IDirect3DDevice9* dev);
void Revalidate(IDirect3DDevice9* dev);
void AfterPresent(IDirect3DDevice9* dev);
void WorldFinished(IDirect3DDevice9* dev);
bool BindInterface(IDirect3DDevice9* dev);
void UnbindInterface();
} // namespace widescreen::hud
namespace widescreen::photo {
bool Wanted(); // photo.cpp: needs the hub's world-finished point (the picture point)
void WorldDone(IDirect3DDevice9* dev);
} // namespace widescreen::photo
namespace widescreen::remix {
bool Wanted(const ds2::Ini& ini);
void DeviceCreated(IDirect3DDevice9* dev, UINT width, UINT height);
bool OnSetCursor();
} // namespace widescreen::remix
namespace {
enum class Mode { Off, Window, Borderless };
Mode g_mode = Mode::Borderless;
enum class MenuScale { Smooth, Sharp, Integer };
bool g_remix = false; // RTX Remix profile (RemixProfile)
MenuScale g_menuScale = MenuScale::Sharp;
int g_width = 0, g_height = 0; // requested resolution (0 = desktop)
HWND g_wnd = nullptr;
WNDPROC g_origProc = nullptr;
std::atomic<LONG> g_vw{800}, g_vh{600}; // the game's idea of its client size
RECT g_mon{}; // monitor rect (screen coordinates) = real client area
using GetCommandLineA_t = LPSTR(WINAPI*)();
using CreateWindowExA_t = HWND(WINAPI*)(DWORD, LPCSTR, LPCSTR, DWORD, int, int, int, int, HWND, HMENU, HINSTANCE, LPVOID);
using MoveWindow_t = BOOL(WINAPI*)(HWND, int, int, int, int, BOOL);
using SetWindowPos_t = BOOL(WINAPI*)(HWND, HWND, int, int, int, int, UINT);
using GetClientRect_t = BOOL(WINAPI*)(HWND, LPRECT);
using PointConv_t = BOOL(WINAPI*)(HWND, LPPOINT);
using SetWindowLongA_t = LONG(WINAPI*)(HWND, int, LONG);
using AdjustWindowRect_t = BOOL(WINAPI*)(LPRECT, DWORD, BOOL);
using GetCursorPos_t = BOOL(WINAPI*)(LPPOINT);
using SetCursorPos_t = BOOL(WINAPI*)(int, int);
GetCommandLineA_t g_GetCommandLineA;
CreateWindowExA_t g_CreateWindowExA;
MoveWindow_t g_MoveWindow;
SetWindowPos_t g_SetWindowPos;
GetClientRect_t g_GetClientRect;
PointConv_t g_ScreenToClient, g_ClientToScreen;
SetWindowLongA_t g_SetWindowLongA;
AdjustWindowRect_t g_AdjustWindowRect;
GetClientRect_t g_GetWindowRect;
GetCursorPos_t g_GetCursorPos;
SetCursorPos_t g_SetCursorPos;
constexpr DWORD kPopupStyle = WS_POPUP | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_SYSMENU | WS_MINIMIZEBOX;
bool Virtualized(HWND h) { return g_mode == Mode::Borderless && h && h == g_wnd; }
int MonW() { return g_mon.right - g_mon.left; }
int MonH() { return g_mon.bottom - g_mon.top; }
// Where the game's virtual client area is shown inside the real client area.
RECT DestRect() {
LONG vw = g_vw, vh = g_vh;
int mw = MonW(), mh = MonH();
RECT r{0, 0, mw, mh};
if (vw <= 0 || vh <= 0 || (vw == mw && vh == mh)) return r;
if (g_menuScale == MenuScale::Integer && vw <= mw && vh <= mh) { // largest whole multiple, centred
LONG k = (mw / vw < mh / vh) ? mw / vw : mh / vh;
r.left = (mw - vw * k) / 2;
r.top = (mh - vh * k) / 2;
r.right = r.left + vw * k;
r.bottom = r.top + vh * k;
return r;
}
if (static_cast<LONGLONG>(mw) * vh > static_cast<LONGLONG>(mh) * vw) { // wider than the content: bars left/right
int w = static_cast<int>(static_cast<LONGLONG>(mh) * vw / vh);
r.left = (mw - w) / 2;
r.right = r.left + w;
} else {
int h = static_cast<int>(static_cast<LONGLONG>(mw) * vh / vw);
r.top = (mh - h) / 2;
r.bottom = r.top + h;
}
return r;
}
POINT RealToVirtual(LONG x, LONG y) {
RECT d = DestRect();
LONG dw = d.right - d.left, dh = d.bottom - d.top;
if (dw <= 0 || dh <= 0) return {x, y};
return {static_cast<LONG>((static_cast<LONGLONG>(x) - d.left) * g_vw / dw),
static_cast<LONG>((static_cast<LONGLONG>(y) - d.top) * g_vh / dh)};
}
POINT VirtualToReal(LONG x, LONG y) {
RECT d = DestRect();
LONG vw = g_vw, vh = g_vh;
if (vw <= 0 || vh <= 0) return {x, y};
return {static_cast<LONG>(d.left + static_cast<LONGLONG>(x) * (d.right - d.left) / vw),
static_cast<LONG>(d.top + static_cast<LONGLONG>(y) * (d.bottom - d.top) / vh)};
}
// Virtual screen position of the game's client (0,0): the top-left of the area it is shown in.
POINT VirtualOrigin() {
RECT d = DestRect();
return {g_mon.left + d.left, g_mon.top + d.top};
}
POINT RealScreenToVirtual(POINT p) {
POINT v = RealToVirtual(p.x - g_mon.left, p.y - g_mon.top), o = VirtualOrigin();
return {v.x + o.x, v.y + o.y};
}
POINT VirtualScreenToReal(POINT p) {
POINT o = VirtualOrigin(), r = VirtualToReal(p.x - o.x, p.y - o.y);
return {r.x + g_mon.left, r.y + g_mon.top};
}
void PaintBars() {
if (!g_wnd) return;
RECT d = DestRect();
HDC dc = GetDC(g_wnd);
if (!dc) return;
auto black = static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH));
RECT parts[4] = {{0, 0, MonW(), d.top}, {0, d.bottom, MonW(), MonH()}, {0, d.top, d.left, d.bottom},
{d.right, d.top, MonW(), d.bottom}};
for (auto& p : parts)
if (p.right > p.left && p.bottom > p.top) FillRect(dc, &p, black);
ReleaseDC(g_wnd, dc);
}
void UpdateMonitor(HWND h) {
MONITORINFO mi{sizeof(mi)};
HMONITOR m = h ? MonitorFromWindow(h, MONITOR_DEFAULTTOPRIMARY) : MonitorFromPoint({0, 0}, MONITOR_DEFAULTTOPRIMARY);
if (GetMonitorInfoA(m, &mi)) g_mon = mi.rcMonitor;
}
// ---- menu scaling: a monitor-sized back buffer for small devices, scaled by us at Present ----------------
IDirect3DDevice9* g_bigDevice = nullptr; // the device whose back buffer we enlarged (null: none)
UINT g_gameW = 0, g_gameH = 0; // the back-buffer size that device's game asked for
IDirect3DSurface9* g_upscaled = nullptr; // nearest-neighbour upscale of the game's corner
UINT g_upW = 0, g_upH = 0;
bool g_scaleFailed = false;
// The game's own picture, kept across our scaling: the scaled frame is written over the back buffer's corner, and the
// game does not redraw every frame (the intro movies draw only when Bink has a new frame; the other Presents carry no
// draw call at all and rely on the buffer still holding the last picture). RestoreCorner puts it back after Present.
IDirect3DSurface9* g_corner = nullptr;
bool g_cornerValid = false;
void ReleaseUpscaled() {
if (g_upscaled) g_upscaled->Release();
g_upscaled = nullptr;
if (g_corner) g_corner->Release();
g_corner = nullptr;
g_cornerValid = false;
g_upW = g_upH = 0;
}
// Enlarge this back buffer? Only for the windowed, single-sampled devices the game makes smaller than the monitor
// (the 800x600 front end) and that fit inside it; the world's device already has the monitor's size.
bool WantBigBackBuffer(const D3DPRESENT_PARAMETERS* pp) {
return g_mode == Mode::Borderless && g_menuScale != MenuScale::Smooth && !g_scaleFailed && pp && pp->Windowed &&
pp->MultiSampleType == D3DMULTISAMPLE_NONE && pp->BackBufferWidth > 0 && pp->BackBufferHeight > 0 &&
MonW() > 0 && pp->BackBufferWidth <= static_cast<UINT>(MonW()) && pp->BackBufferHeight <= static_cast<UINT>(MonH()) &&
(pp->BackBufferWidth < static_cast<UINT>(MonW()) || pp->BackBufferHeight < static_cast<UINT>(MonH()));
}
// The game's corner -> (nearest-neighbour, whole multiple) g_upscaled -> the fitted rectangle of the back buffer
// (linear for sharp: a downscale by less than one step; a 1:1 copy for integer), black around it.
bool ScaleMenuFrame(IDirect3DDevice9* dev) {
RECT d = DestRect();
UINT dw = d.right - d.left, dh = d.bottom - d.top;
if (!g_gameW || !g_gameH || !dw || !dh) return false;
IDirect3DSurface9* bb = nullptr;
if (FAILED(dev->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &bb))) return false;
D3DSURFACE_DESC desc{};
bb->GetDesc(&desc);
UINT k = (dw + g_gameW - 1) / g_gameW, kh = (dh + g_gameH - 1) / g_gameH;
if (kh > k) k = kh;
if (k < 1) k = 1;
UINT uw = g_gameW * k, uh = g_gameH * k;
HRESULT hr = S_OK;
if (!g_upscaled || g_upW != uw || g_upH != uh) {
ReleaseUpscaled();
hr = dev->CreateRenderTarget(uw, uh, desc.Format, D3DMULTISAMPLE_NONE, 0, FALSE, &g_upscaled, nullptr);
if (SUCCEEDED(hr)) {
g_upW = uw, g_upH = uh;
ds2::log().Write("display: menus %ux%u -> %ux%u (nearest) -> %ux%u (%s)", g_gameW, g_gameH, uw, uh, dw, dh,
g_menuScale == MenuScale::Integer ? "integer" : "sharp");
}
}
RECT src{0, 0, static_cast<LONG>(g_gameW), static_cast<LONG>(g_gameH)};
g_cornerValid = false;
if (SUCCEEDED(hr) && !g_corner) hr = dev->CreateRenderTarget(g_gameW, g_gameH, desc.Format, D3DMULTISAMPLE_NONE, 0, FALSE, &g_corner, nullptr);
if (SUCCEEDED(hr)) hr = dev->StretchRect(bb, &src, g_corner, nullptr, D3DTEXF_POINT);
if (SUCCEEDED(hr)) g_cornerValid = true;
if (SUCCEEDED(hr)) hr = dev->StretchRect(bb, &src, g_upscaled, nullptr, D3DTEXF_POINT);
if (SUCCEEDED(hr)) hr = dev->StretchRect(g_upscaled, nullptr, bb, &d, (uw == dw && uh == dh) ? D3DTEXF_POINT : D3DTEXF_LINEAR);
// Black around the fitted rectangle (the game's corner may reach outside it). After the copy: if that fails,
// the corner is still intact for the fallback.
LONG bw = static_cast<LONG>(desc.Width), bh = static_cast<LONG>(desc.Height);
const RECT around[4] = {{0, 0, bw, d.top}, {0, d.bottom, bw, bh}, {0, d.top, d.left, d.bottom}, {d.right, d.top, bw, d.bottom}};
for (const RECT& r : around)
if (SUCCEEDED(hr) && r.right > r.left && r.bottom > r.top) hr = dev->ColorFill(bb, &r, D3DCOLOR_XRGB(0, 0, 0));
bb->Release();
if (FAILED(hr)) {
ds2::log().Write("display: menu scaling failed (0x%08lX), using the driver's stretch", static_cast<unsigned long>(hr));
g_scaleFailed = true;
ReleaseUpscaled();
return false;
}
return true;
}
// After Present: the game's picture goes back into the corner of the back buffer (see g_corner).
void RestoreCorner(IDirect3DDevice9* dev) {
if (!g_cornerValid || !g_corner) return;
IDirect3DSurface9* bb = nullptr;
if (FAILED(dev->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &bb))) return;
RECT dst{0, 0, static_cast<LONG>(g_gameW), static_cast<LONG>(g_gameH)};
dev->StretchRect(g_corner, nullptr, bb, &dst, D3DTEXF_POINT);
bb->Release();
}
// Present into the destination rectangle (the hub's Present stage: it has patched the renderer's three Present calls).
HRESULT DisplayPresent(IDirect3DDevice9* dev, ds2::d3dhub::PresentFn present) {
void* device = dev;
if (g_mode != Mode::Borderless || !g_wnd) return present(device, nullptr, nullptr, nullptr, nullptr);
widescreen::hud::BeforePresent(static_cast<IDirect3DDevice9*>(device));
RECT d = DestRect();
if (device == g_bigDevice) { // the game drew g_gameW x g_gameH in the corner of a monitor-sized back buffer
if (!g_scaleFailed && ScaleMenuFrame(static_cast<IDirect3DDevice9*>(device))) {
HRESULT hr = present(device, nullptr, nullptr, nullptr, nullptr);
RestoreCorner(static_cast<IDirect3DDevice9*>(device));
return hr;
}
RECT src{0, 0, static_cast<LONG>(g_gameW), static_cast<LONG>(g_gameH)};
return present(device, &src, &d, nullptr, nullptr);
}
bool full = d.left == 0 && d.top == 0 && d.right == MonW() && d.bottom == MonH();
HRESULT hr = present(device, nullptr, full ? nullptr : &d, nullptr, nullptr);
widescreen::hud::AfterPresent(static_cast<IDirect3DDevice9*>(device));
return hr;
}
// ---- the hub's device events (mods/common/d3dhub.h installs the Direct3D hooks once for all plugins) ------------------
bool g_hudOn = false; // the HUD scale hooks are installed (hud::DeviceCreated etc. only then)
// A monitor-sized back buffer when wanted; the game's own parameter block keeps the size it asked for (it reads it back).
// The hub falls back to the game's parameters if the bigger buffer is refused.
bool OnAdjustParams(D3DPRESENT_PARAMETERS* mine, const D3DPRESENT_PARAMETERS* game) {
if (!WantBigBackBuffer(game)) return false;
mine->BackBufferWidth = MonW();
mine->BackBufferHeight = MonH();
return true;
}
void NoteBackBuffer(IDirect3DDevice9* dev, const D3DPRESENT_PARAMETERS* pp, bool big) {
g_bigDevice = big ? dev : (dev == g_bigDevice ? nullptr : g_bigDevice);
if (big) {
g_gameW = pp->BackBufferWidth;
g_gameH = pp->BackBufferHeight;
ds2::log().Write("display: %ux%u device gets a %dx%d back buffer (MenuScale=%s)", g_gameW, g_gameH, MonW(), MonH(),
g_menuScale == MenuScale::Integer ? "integer" : "sharp");
}
}
void OnBeforeCreate() {
ReleaseUpscaled(); // the game replaces its device (front end <-> world)
g_bigDevice = nullptr;
}
void OnBeforeReset(IDirect3DDevice9* dev) {
if (dev == g_bigDevice) ReleaseUpscaled(); // default-pool resources must go before a reset
if (g_hudOn) widescreen::hud::BeforeReset();
}
void OnCreated(IDirect3DDevice9* dev, const D3DPRESENT_PARAMETERS* pp, bool big) {
NoteBackBuffer(dev, pp, big);
if (g_remix) { // Remix profile: only the remix.cpp hooks (sky, off-screen passes)
widescreen::remix::DeviceCreated(dev, pp->BackBufferWidth, pp->BackBufferHeight);
} else if (g_hudOn) {
widescreen::hud::DeviceCreated(dev);
}
}
void OnAfterReset(IDirect3DDevice9* dev, const D3DPRESENT_PARAMETERS* pp, bool big) { NoteBackBuffer(dev, pp, big); }
void OnFrame(IDirect3DDevice9* dev) { widescreen::hud::Revalidate(dev); }
bool g_revalidate = true; // [Display] RevalidateHooks=0 turns the per-frame check off (diagnostics; all plugins)
void LogWheel(UINT msg, WPARAM wp);
LRESULT CALLBACK DisplayWndProc(HWND h, UINT msg, WPARAM wp, LPARAM lp) {
if (msg == WM_MOUSEWHEEL) LogWheel(msg, wp);
// RTX Remix: the game's D3D9 hardware cursor is set here (remix.cpp RemixCursor)
if (g_remix && msg == WM_SETCURSOR && LOWORD(lp) == HTCLIENT && widescreen::remix::OnSetCursor()) return TRUE;
if (g_mode == Mode::Borderless) {
if (msg >= WM_MOUSEFIRST && msg <= WM_MOUSELAST && msg != WM_MOUSEWHEEL && msg != WM_MOUSEHWHEEL) {
POINT v = RealToVirtual(static_cast<short>(LOWORD(lp)), static_cast<short>(HIWORD(lp)));
lp = MAKELPARAM(static_cast<WORD>(v.x), static_cast<WORD>(v.y));
} else if (msg == WM_MOUSEWHEEL || msg == WM_MOUSEHWHEEL) { // screen coordinates
POINT v = RealScreenToVirtual({static_cast<short>(LOWORD(lp)), static_cast<short>(HIWORD(lp))});
lp = MAKELPARAM(static_cast<WORD>(v.x), static_cast<WORD>(v.y));
} else if (msg == WM_SIZE && wp != SIZE_MINIMIZED) {
lp = MAKELPARAM(static_cast<WORD>(g_vw.load()), static_cast<WORD>(g_vh.load()));
} else if (msg == WM_ERASEBKGND) {
PaintBars();
return 1;
} else if (msg == WM_GETMINMAXINFO) {
LRESULT r = CallWindowProcA(g_origProc, h, msg, wp, lp);
auto* mmi = reinterpret_cast<MINMAXINFO*>(lp);
mmi->ptMaxSize = {MonW(), MonH()};
mmi->ptMaxTrackSize = {MonW(), MonH()};
return r;
} else if (msg == WM_DISPLAYCHANGE) {
UpdateMonitor(h);
}
}
return CallWindowProcA(g_origProc, h, msg, wp, lp);
}
// ---- API hooks ----------------------------------------------------------------------------------
char g_cmdline[2048];
LPSTR WINAPI HookGetCommandLineA() {
return g_cmdline[0] ? g_cmdline : g_GetCommandLineA();
}
HWND WINAPI HookCreateWindowExA(DWORD ex, LPCSTR cls, LPCSTR title, DWORD style, int x, int y, int w, int h, HWND parent,
HMENU menu, HINSTANCE inst, LPVOID param) {
bool main = !g_wnd && cls && reinterpret_cast<UINT_PTR>(cls) > 0xffff && _strnicmp(cls, "gpgwndclass_", 12) == 0;
// Create at the size the game asks for (it sizes itself from the first WM_SIZE), without a frame and
// hidden; then put our translation in front of the game's window procedure and only then cover the monitor.
DWORD shown = style & WS_VISIBLE;
if (main && g_mode == Mode::Borderless) {
style = kPopupStyle;
ex &= ~(WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE | WS_EX_DLGMODALFRAME);
if (w > 0 && h > 0 && w != CW_USEDEFAULT && h != CW_USEDEFAULT) {
g_vw = w;
g_vh = h;
}
}
HWND wnd = g_CreateWindowExA(ex, cls, title, style, x, y, w, h, parent, menu, inst, param);
if (main && wnd) {
g_wnd = wnd;
if (g_mode == Mode::Borderless) {
g_origProc = reinterpret_cast<WNDPROC>(
g_SetWindowLongA(wnd, GWL_WNDPROC, reinterpret_cast<LONG>(DisplayWndProc)));
UpdateMonitor(wnd);
g_SetWindowPos(wnd, nullptr, g_mon.left, g_mon.top, MonW(), MonH(),
SWP_NOZORDER | SWP_NOACTIVATE | (shown ? SWP_SHOWWINDOW : 0));
ds2::log().Write("display: borderless game window %p on monitor %ldx%ld, game size %ldx%ld",
static_cast<void*>(wnd), static_cast<long>(MonW()), static_cast<long>(MonH()),
static_cast<long>(g_vw.load()), static_cast<long>(g_vh.load()));
}
}
return wnd;
}
void SetVirtualSize(int w, int h) {
if (w <= 0 || h <= 0) return;
if (g_vw != w || g_vh != h) ds2::log().Write("display: game size %dx%d", w, h);
g_vw = w;
g_vh = h;
}
// The game's resize requests change only its virtual size; the real window keeps covering the monitor. A
// synthetic WM_SIZE goes to the game only when that size really changed (a restored WM_SIZE also marks the
// game active again, sub_5ebc6f), and never while minimized; a re-entry guard stops feedback loops.
bool g_resizing = false;
void NotifyGameSize(HWND h, int w, int hh) {
LONG ow = g_vw, oh = g_vh;
SetVirtualSize(w, hh);
if ((ow == g_vw && oh == g_vh) || IsIconic(h) || g_resizing) return;
g_resizing = true;
SendMessageA(h, WM_SIZE, SIZE_RESTORED, MAKELPARAM(static_cast<WORD>(w), static_cast<WORD>(hh)));
g_resizing = false;
PaintBars();
}
BOOL WINAPI HookMoveWindow(HWND h, int x, int y, int w, int hh, BOOL repaint) {
if (!Virtualized(h)) return g_MoveWindow(h, x, y, w, hh, repaint);
UpdateMonitor(h);
BOOL ok = g_MoveWindow(h, g_mon.left, g_mon.top, MonW(), MonH(), repaint);
if (ok) NotifyGameSize(h, w, hh);
return ok;
}
BOOL WINAPI HookSetWindowPos(HWND h, HWND after, int x, int y, int w, int hh, UINT flags) {
if (!Virtualized(h)) return g_SetWindowPos(h, after, x, y, w, hh, flags);
bool sized = !(flags & SWP_NOSIZE);
UpdateMonitor(h);
// Only a size request needs our full-monitor geometry; z-order/show-only calls pass their flags through.
BOOL ok = sized ? g_SetWindowPos(h, after, g_mon.left, g_mon.top, MonW(), MonH(), flags & ~SWP_NOMOVE)
: g_SetWindowPos(h, after, 0, 0, 0, 0, flags | SWP_NOMOVE | SWP_NOSIZE);
if (ok && sized) NotifyGameSize(h, w, hh);
return ok;
}
BOOL WINAPI HookGetClientRect(HWND h, LPRECT r) {
if (!Virtualized(h) || !r || IsIconic(h)) return g_GetClientRect(h, r);
*r = {0, 0, g_vw, g_vh};
return TRUE;
}
BOOL WINAPI HookScreenToClient(HWND h, LPPOINT p) {
if (!p || !Virtualized(h)) return g_ScreenToClient(h, p);
POINT o = VirtualOrigin();
p->x -= o.x;
p->y -= o.y;
return TRUE;
}
BOOL WINAPI HookClientToScreen(HWND h, LPPOINT p) {
if (!p || !Virtualized(h)) return g_ClientToScreen(h, p);
POINT o = VirtualOrigin();
p->x += o.x;
p->y += o.y;
return TRUE;
}
BOOL WINAPI HookGetWindowRect(HWND h, LPRECT r) {
if (!r || !Virtualized(h) || IsIconic(h)) return g_GetWindowRect(h, r);
POINT o = VirtualOrigin();
*r = {o.x, o.y, o.x + g_vw, o.y + g_vh};
return TRUE;
}
// [Display] LogCursor=1 (diagnostic, off by default): the camera's mouse rotation recentres the cursor with
// SetCursorPos every frame and measures the next GetCursorPos against the centre. Logs once a second how that
// next read relates to the position just set: exact, still the old position (the warp not applied yet), or moved.
struct CursorStats {
bool on = false, awaiting = false;
POINT set{}, pre{};
LARGE_INTEGER setAt{}, windowStart{}, freq{};
unsigned sets = 0, gets = 0, exact = 0, stale = 0, moved = 0;
double offSum = 0, offMax = 0, gapSum = 0;
} g_cs;
void CursorStatsRead(POINT raw) {
CursorStats& c = g_cs;
++c.gets;
if (!c.awaiting) return;
c.awaiting = false;
LARGE_INTEGER now;
QueryPerformanceCounter(&now);
c.gapSum += static_cast<double>(now.QuadPart - c.setAt.QuadPart) * 1e6 / static_cast<double>(c.freq.QuadPart);
double dx = raw.x - c.set.x, dy = raw.y - c.set.y, off = dx * dx + dy * dy;
if (off == 0) {
++c.exact;
} else if (raw.x == c.pre.x && raw.y == c.pre.y) {
++c.stale;
} else {
++c.moved;
off = std::sqrt(off);
c.offSum += off;
if (off > c.offMax) c.offMax = off;
}
}
void CursorStatsSet(POINT set) {
CursorStats& c = g_cs;
LARGE_INTEGER now;
QueryPerformanceCounter(&now);
if (c.sets && (now.QuadPart - c.windowStart.QuadPart) > c.freq.QuadPart) {
unsigned n = c.exact + c.stale + c.moved;
ds2::log().Write("cursor: %u recentres, %u reads in %.1f s; read after a recentre: %u exact, %u still the old "
"position, %u elsewhere (avg %.1f px, max %.0f px off); avg %.0f us after the recentre",
c.sets, c.gets,
static_cast<double>(now.QuadPart - c.windowStart.QuadPart) / static_cast<double>(c.freq.QuadPart),
c.exact, c.stale, c.moved, c.moved ? c.offSum / c.moved : 0.0, c.offMax, n ? c.gapSum / n : 0.0);
c.sets = c.gets = c.exact = c.stale = c.moved = 0;
c.offSum = c.offMax = c.gapSum = 0;
}
if (!c.sets) c.windowStart = now;
++c.sets;
c.set = set;
c.setAt = now;
c.awaiting = true;
}
// With LogCursor: every wheel message the window receives (a notch is 120; smooth-scrolling wheels send less).
void LogWheel(UINT, WPARAM wp) {
if (g_cs.on) ds2::log().Write("cursor: wheel %d", static_cast<int>(static_cast<short>(HIWORD(wp))));
}
BOOL WINAPI HookGetCursorPos(LPPOINT p) {
BOOL ok = g_GetCursorPos(p);
if (ok && p && g_cs.on) CursorStatsRead(*p);
if (ok && p && g_wnd && g_mode == Mode::Borderless) *p = RealScreenToVirtual(*p);
return ok;
}
BOOL WINAPI HookSetCursorPos(int x, int y) {
if (g_wnd && g_mode == Mode::Borderless) {
POINT r = VirtualScreenToReal({x, y});
x = r.x;
y = r.y;
}
if (g_cs.on) {
g_GetCursorPos(&g_cs.pre);
BOOL ok = g_SetCursorPos(x, y);
CursorStatsSet({x, y});
return ok;
}
return g_SetCursorPos(x, y);
}
// The game sizes its window with AdjustWindowRect(style it intends: caption + border) and compares the result
// with the screen (sub_5f109a, sub_5ebeba). A full-screen client plus borders never fits, so the game falls
// back to its fullscreen path. The borderless window has no border: say so.
BOOL WINAPI HookAdjustWindowRect(LPRECT r, DWORD style, BOOL menu) {
if (g_mode == Mode::Borderless) return r != nullptr;
return g_AdjustWindowRect(r, style, menu);
}
LONG WINAPI HookSetWindowLongA(HWND h, int index, LONG value) {
if (Virtualized(h)) {
if (index == GWL_STYLE) value = kPopupStyle | (value & (WS_VISIBLE | WS_MINIMIZE));
if (index == GWL_EXSTYLE) value &= ~(WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE | WS_EX_DLGMODALFRAME);
if (index == GWL_WNDPROC) { // keep our translation in front of a new game procedure
LONG prev = reinterpret_cast<LONG>(g_origProc);
g_origProc = reinterpret_cast<WNDPROC>(value);
return prev;
}
}
return g_SetWindowLongA(h, index, value);
}
bool HasArg(const char* cmd, const char* key) {
size_t n = std::strlen(key);
for (const char* p = cmd; *p; ++p)
if ((p == cmd || p[-1] == ' ' || p[-1] == '"') && _strnicmp(p, key, n) == 0) return true;
return false;
}
// Frame cap: the frame function (sub_5efd3e) waits out each frame to "maxfps" (default 75, AppModule+0x7c) when
// the game runs windowed (flag 0x400000 set by default; 0x200000 is the fullscreen equivalent, set only with an
// explicit maxfps). Our borderless window is windowed, so every player was held at 75 fps. Pass maxfps on the
// command line instead: the monitor's refresh rate by default ([Display] MaxFps=refresh), 0 = no cap, or a number.
// An explicit maxfps on the command line wins.
char g_maxFps[16] = "refresh";
int MaxFpsValue() {
if (_stricmp(g_maxFps, "refresh") != 0) return std::atoi(g_maxFps);
DEVMODEA dm{};
dm.dmSize = sizeof(dm);
if (EnumDisplaySettingsA(nullptr, ENUM_CURRENT_SETTINGS, &dm) && dm.dmDisplayFrequency > 1) return static_cast<int>(dm.dmDisplayFrequency);
return -1; // unknown: leave the game's default
}
void BuildCommandLine() {
const char* orig = g_GetCommandLineA();
int w = g_width, h = g_height;
if (w <= 0 || h <= 0) {
UpdateMonitor(nullptr);
w = MonW();
h = MonH();
}
char extra[128] = "";
if (!HasArg(orig, "fullscreen=")) strcat_s(extra, " fullscreen=false");
// A desktop-sized window with a title bar does not fit the desktop (the game would switch to fullscreen),
// so Mode=window only sets a resolution when one is configured.
bool setRes = g_mode == Mode::Borderless || g_width > 0;
if (setRes && !HasArg(orig, "width=") && !HasArg(orig, "height=")) {
char res[48];
sprintf_s(res, " width=%d height=%d", w, h);
strcat_s(extra, res);
}
int fps = MaxFpsValue();
if (fps >= 0 && !HasArg(orig, "maxfps=")) {
char cap[24];
sprintf_s(cap, " maxfps=%d", fps);
strcat_s(extra, cap);
}
sprintf_s(g_cmdline, "%s%s", orig, extra);
ds2::log().Write("display: command line +%s", extra[0] ? extra : " (nothing: set on the command line)");
}
// ---- device reset: retry instead of dying ----------------------------------------------------------
// Rapi's re-create path (sub_508260, after IDirect3DDevice9::Reset) treats a failed GetBackBuffer as fatal
// ("Video initialization failure! ... GetBackBuffer ... 0x8876086C"): seen with the Steam overlay. Returning
// false there instead is safe - the caller (sub_509550 <- Rapi::Begin) skips the frame and tries the reset
// again on the next one. Only a failure that lasts 10 s goes on to the game's original error.
// Site = "push ecx; mov esi,esp; call <report helper>" right after the failed GetBackBuffer (Steam 0x50844b,
// Broken World 0x51671b); found by signature, the jump targets are read from the code around it:
// +8 continues the error report, the "je" at +0x14 leads to "xor al,al; ...; ret" (return false).
constexpr char kBackBufferFailPattern[] =
"53 53 50 FF 52 48 8B F8 3B FB ?? ?? ?? ?? ?? ?? 51 8B F4 E8 ?? ?? ?? ?? 89 06 E8 ?? ?? ?? ?? 83 C4 04";
constexpr DWORD kBackBufferFailPatternOffset = 16;
DWORD g_bbReturnFalse = 0, g_bbContinue = 0, g_bbReportHelper = 0;
DWORD g_failFirst = 0, g_failLast = 0, g_failCount = 0;
BOOL __stdcall ReportBackBufferFailure(HRESULT hr) {
DWORD now = GetTickCount();
if (now - g_failLast > 2000) { // a new streak
g_failFirst = now;
g_failCount = 0;
}
g_failLast = now;
if (++g_failCount <= 3 || g_failCount % 100 == 0)
ds2::log().Write("display: device reset failed (GetBackBuffer 0x%08lX), retrying (%lu)", static_cast<unsigned long>(hr),
g_failCount);
return now - g_failFirst > 10000;
}
__declspec(naked) void BackBufferFailStub() {
__asm {
push ecx // preserve, the original path starts with push ecx
push edi // HRESULT of GetBackBuffer
call ReportBackBufferFailure
pop ecx
test eax, eax
jnz report
push g_bbReturnFalse // xor al,al; restore registers; return false
ret
report:
push ecx // the replaced instructions...
mov esi, esp
mov eax, g_bbReportHelper
call eax
push g_bbContinue // ...and on with the game's error report
ret
}
}
} // namespace
// For the dev harness (ds2dev): a point in the GAME's client coordinates -> real client (screen=FALSE) or
// real screen (screen=TRUE) coordinates, so injected mouse input lands where the game expects it.
#pragma comment(linker, "/EXPORT:DS2GameToReal=_DS2GameToReal@8")
extern "C" BOOL __stdcall DS2GameToReal(POINT* p, BOOL screen) {
if (!p || !g_wnd) return FALSE;
if (g_mode == Mode::Borderless) {
*p = VirtualToReal(p->x, p->y);
if (screen) {
p->x += g_mon.left;
p->y += g_mon.top;
}
return TRUE;
}
return screen ? g_ClientToScreen ? g_ClientToScreen(g_wnd, p) : ClientToScreen(g_wnd, p) : TRUE;
}
namespace widescreen {
void MonitorSize(int* w, int* h) {
*w = MonW();
*h = MonH();
}
// The size the game's world is (or will be) created at: [Display] Resolution=WxH, else the monitor. The HUD scale
// recognises the world by this size (the front end runs at 800x600 / 1024x768).
void WorldSize(int* w, int* h) {
*w = g_width > 0 && g_height > 0 ? g_width : MonW();
*h = g_width > 0 && g_height > 0 ? g_height : MonH();
}
bool Borderless() { return g_mode == Mode::Borderless && g_wnd; }
// [Graphics] Renderer=auto (default) | remix | d3d9 (ds2::RemixProfile, shared with DS2Graphics). NVIDIA RTX Remix replaces
// d3d9.dll and keeps its 64-bit renderer in a ".trex" folder beside the game exe; auto = remix when ".trex\NvRemixBridge.exe"
// is there. The remix profile leaves the Direct3D device alone except for remix.cpp (RemixSky, RemixSkipOffscreen): driver
// menu stretch, no HUD layer, no INTZ depth, no sampler changes. DS2Graphics does nothing in it (no SMAA, sharpening, SSAO,
// anisotropy; the game's own Shaders handling, fixed-function).
void InstallDisplay(const ds2::Ini& ini) {
char mode[32], res[32];
ini.String("Display", "Mode", "borderless", mode, sizeof(mode));
ini.String("Display", "Resolution", "desktop", res, sizeof(res));
ini.String("Display", "MaxFps", "refresh", g_maxFps, sizeof(g_maxFps));
g_cs.on = ini.Bool("Display", "LogCursor", false);
QueryPerformanceFrequency(&g_cs.freq);
g_mode = _stricmp(mode, "off") == 0 ? Mode::Off : _stricmp(mode, "window") == 0 ? Mode::Window : Mode::Borderless;
// The game's own exclusive fullscreen (Mode=off, or fullscreen=true on the command line) has no mouse pointer on Windows 10
// 1703+: the game hides the Win32 pointer and draws its own through the Direct3D device cursor, which then never shows.
// Skipping the two ShowCursor(FALSE) sites brings it back (common/exclusive_cursor.h). Borderless / window never need it.
if (ini.Bool("Display", "ExclusiveCursor", true)) {
if (ds2::excursor::Applies(true, mode, GetCommandLineA()))
ds2::log().Write("display: exclusive fullscreen pointer: %d of 2 sites skipped", ds2::excursor::Apply());
else
ds2::log().Write("display: exclusive fullscreen pointer: not needed (Mode=%s, the game runs windowed)", mode);
}
DWORD bb = ds2::FindUnique(kBackBufferFailPattern);
if (ini.Bool("Display", "RetryDeviceReset", true) && bb) {
DWORD site = bb + kBackBufferFailPatternOffset;
auto at = [](DWORD a) { return *reinterpret_cast<const BYTE*>(a); };
auto rel32 = [](DWORD a) { return a + 4 + *reinterpret_cast<const LONG*>(a); };
g_bbReportHelper = rel32(site + 4); // call target
g_bbContinue = site + 8;
DWORD je = site + 0x14; // 0F 84 rel32
if (at(je) == 0x0F && at(je + 1) == 0x84) g_bbReturnFalse = rel32(je + 2);
if (g_bbReturnFalse && at(g_bbReturnFalse) == 0x32 && at(g_bbReturnFalse + 1) == 0xC0) { // xor al,al
BYTE orig[8], patch[8] = {0xE9, 0, 0, 0, 0, 0x90, 0x90, 0x90};
std::memcpy(orig, reinterpret_cast<const void*>(site), sizeof(orig));
LONG rel = static_cast<LONG>(reinterpret_cast<DWORD>(&BackBufferFailStub) - (site + 5));
std::memcpy(patch + 1, &rel, 4);
ds2::PatchBytes(site, orig, patch, sizeof(patch), "display: retry a failed device reset instead of a fatal error");
} else {
ds2::log().Write("SKIP display: device-reset retry (unexpected code near %08lX)", site);
}
} else if (!bb) {
ds2::log().Write("SKIP display: device-reset retry (signature not found)");
}
// Direct3D: menu scaling, the HUD layer, the Present into the fitted rectangle and the Remix hooks (borderless only, checked
// when they act), photo mode's picture point (any mode). The hooks themselves are the shared hub's (d3dhub.h); picture
// quality is DS2Graphics'.
char menuScale[16];
g_revalidate = ini.Bool("Display", "RevalidateHooks", true);
ini.String("Display", "MenuScale", "sharp", menuScale, sizeof(menuScale));
g_menuScale = _stricmp(menuScale, "smooth") == 0 ? MenuScale::Smooth
: _stricmp(menuScale, "integer") == 0 ? MenuScale::Integer
: MenuScale::Sharp;
char renderer[16];
ini.String("Graphics", "Renderer", "auto", renderer, sizeof(renderer), "Display");
bool remix = g_remix = ds2::RemixProfile(renderer);
if (remix) {
g_menuScale = MenuScale::Smooth;
ds2::log().Write("display: RTX Remix profile (Renderer): MenuScale=smooth, HudScale=1; Direct3D hooks only for RemixSky/RemixSkipOffscreen");
}
bool borderless = g_mode == Mode::Borderless;
bool menus = borderless && g_menuScale != MenuScale::Smooth;
bool remixHooks = remix && widescreen::remix::Wanted(ini);
bool hud = !remix && borderless && widescreen::hud::Wanted(ini);
bool photo = !remix && widescreen::photo::Wanted();
if (menus || hud || photo || remixHooks || borderless) {
const ds2::d3dhub::Api* hub = ds2::d3dhub::Connect("DS2Widescreen");
if (hub) {
if (!g_revalidate) hub->SetRevalidate(false);
ds2::d3dhub::Callbacks cb;
cb.name = "DS2Widescreen display";
cb.priority = ds2::d3dhub::kPrioHud;
if (menus) cb.AdjustParams = OnAdjustParams;
if (menus || hud || remixHooks) {
cb.BeforeCreate = OnBeforeCreate;
cb.BeforeReset = OnBeforeReset;
cb.Created = OnCreated;
cb.AfterReset = OnAfterReset;
}
if (hud) cb.Frame = OnFrame;
if (borderless) cb.Present = DisplayPresent; // Present into the fitted rectangle, the HUD layer composite
DWORD haveMain = (menus || hud || remixHooks || borderless) ? hub->Register(&cb) : 0; // photo mode registers its own below
if (hud) {
ds2::d3dhub::Callbacks h;
h.name = "DS2Widescreen HUD scale";
h.priority = ds2::d3dhub::kPrioHud;
h.WorldFinished = widescreen::hud::WorldFinished;
h.BindInterface = widescreen::hud::BindInterface;
h.UnbindInterface = widescreen::hud::UnbindInterface;
DWORD have = hub->Register(&h) | (haveMain & ds2::d3dhub::kHavePresent); // (the composite is the display callback's Present)
const DWORD need = ds2::d3dhub::kHaveDevice | ds2::d3dhub::kHavePresent | ds2::d3dhub::kHaveWorld;
if ((have & need) != need) {
ds2::log().Write("SKIP hud: the Direct3D hooks it needs are not all available (0x%lX of 0x%lX), HUD not scaled", have, need);
} else {
g_hudOn = widescreen::hud::Install();
}
}
if (photo) {
ds2::d3dhub::Callbacks ph;
ph.name = "DS2Widescreen photo mode";
ph.priority = ds2::d3dhub::kPrioPhoto; // after the anti-aliasing (DS2Graphics, 100): the picture has it
ph.WorldFinished = widescreen::photo::WorldDone;
if (!(hub->Register(&ph) & ds2::d3dhub::kHaveWorld))
ds2::log().Write("SKIP photo: the world-finished point is not available - photo mode takes no picture");
}
}
}
if (g_mode == Mode::Off) return;
if (sscanf_s(res, "%dx%d", &g_width, &g_height) != 2) g_width = g_height = 0;
SetProcessDPIAware(); // real pixels: no DPI bitmap scaling of the game window
g_GetCommandLineA = static_cast<GetCommandLineA_t>(
ds2::HookImport("KERNEL32.dll", "GetCommandLineA", reinterpret_cast<void*>(HookGetCommandLineA), "GetCommandLineA (display)"));
if (!g_GetCommandLineA) return;
BuildCommandLine();
if (g_mode != Mode::Borderless) return;
g_SetWindowLongA = static_cast<SetWindowLongA_t>(
ds2::HookImport("USER32.dll", "SetWindowLongA", reinterpret_cast<void*>(HookSetWindowLongA), "SetWindowLongA (display)"));
g_AdjustWindowRect = static_cast<AdjustWindowRect_t>(
ds2::HookImport("USER32.dll", "AdjustWindowRect", reinterpret_cast<void*>(HookAdjustWindowRect), "AdjustWindowRect (display)"));
g_CreateWindowExA = static_cast<CreateWindowExA_t>(
ds2::HookImport("USER32.dll", "CreateWindowExA", reinterpret_cast<void*>(HookCreateWindowExA), "CreateWindowExA (display)"));
g_MoveWindow = static_cast<MoveWindow_t>(
ds2::HookImport("USER32.dll", "MoveWindow", reinterpret_cast<void*>(HookMoveWindow), "MoveWindow (display)"));
g_SetWindowPos = static_cast<SetWindowPos_t>(
ds2::HookImport("USER32.dll", "SetWindowPos", reinterpret_cast<void*>(HookSetWindowPos), "SetWindowPos (display)"));
g_GetClientRect = static_cast<GetClientRect_t>(
ds2::HookImport("USER32.dll", "GetClientRect", reinterpret_cast<void*>(HookGetClientRect), "GetClientRect (display)"));
g_ScreenToClient = static_cast<PointConv_t>(
ds2::HookImport("USER32.dll", "ScreenToClient", reinterpret_cast<void*>(HookScreenToClient), "ScreenToClient (display)"));
g_GetWindowRect = static_cast<GetClientRect_t>(
ds2::HookImport("USER32.dll", "GetWindowRect", reinterpret_cast<void*>(HookGetWindowRect), "GetWindowRect (display)"));
g_GetCursorPos = static_cast<GetCursorPos_t>(
ds2::HookImport("USER32.dll", "GetCursorPos", reinterpret_cast<void*>(HookGetCursorPos), "GetCursorPos (display)"));
g_SetCursorPos = static_cast<SetCursorPos_t>(
ds2::HookImport("USER32.dll", "SetCursorPos", reinterpret_cast<void*>(HookSetCursorPos), "SetCursorPos (display)"));
g_ClientToScreen = static_cast<PointConv_t>(
ds2::HookImport("USER32.dll", "ClientToScreen", reinterpret_cast<void*>(HookClientToScreen), "ClientToScreen (display)"));
}
} // namespace widescreen