1152 lines
55 KiB
C++
1152 lines
55 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 BeforeCreate();
|
|
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);
|
|
bool BindRadar(IDirect3DDevice9* dev, void* radar);
|
|
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 };
|
|
// [Display] MenuBars: what fills the side bars around the menus (prototype). black = nothing; blur = the menu picture covering the
|
|
// whole screen, strongly blurred and dark; blurlight = lighter blur, less dark; mirror = the picture mirrored outwards, dark.
|
|
enum class MenuBars { Black, Blur, BlurLight, Mirror };
|
|
MenuBars g_bars = MenuBars::Blur;
|
|
bool g_barsFailed = false;
|
|
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
|
|
IDirect3DTexture9* g_upscaledTex = nullptr; // the same surface as a texture (only with MenuBars: the mirror look samples it)
|
|
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;
|
|
|
|
// MenuBars resources (default pool: released before every Reset and new device with the rest).
|
|
constexpr int kMaxBlurLevels = 4;
|
|
struct BlurLevel {
|
|
IDirect3DTexture9* tex = nullptr;
|
|
IDirect3DSurface9* surf = nullptr;
|
|
UINT w = 0, h = 0;
|
|
};
|
|
BlurLevel g_level[kMaxBlurLevels]; // the picture halved again and again (linear StretchRect = a 2x2 box each step)
|
|
int g_levelCount = 0;
|
|
UINT g_levelCropW = 0, g_levelCropH = 0;
|
|
IDirect3DStateBlock9* g_barState = nullptr;
|
|
IDirect3DQuery9 *g_qDisjoint = nullptr, *g_qStart = nullptr, *g_qEnd = nullptr, *g_qFreq = nullptr; // DS2_MENUBARS_STATS only
|
|
|
|
void ReleaseBars() {
|
|
for (BlurLevel& l : g_level) {
|
|
if (l.surf) l.surf->Release();
|
|
if (l.tex) l.tex->Release();
|
|
l = BlurLevel();
|
|
}
|
|
g_levelCount = 0;
|
|
g_levelCropW = g_levelCropH = 0;
|
|
if (g_barState) g_barState->Release();
|
|
g_barState = nullptr;
|
|
}
|
|
|
|
void ReleaseUpscaled() {
|
|
ReleaseBars();
|
|
for (IDirect3DQuery9** q : {&g_qDisjoint, &g_qStart, &g_qEnd, &g_qFreq}) { // (not in ReleaseBars: DrawBars calls that mid-measurement)
|
|
if (*q) (*q)->Release();
|
|
*q = nullptr;
|
|
}
|
|
if (g_upscaled) g_upscaled->Release();
|
|
g_upscaled = nullptr;
|
|
if (g_upscaledTex) g_upscaledTex->Release();
|
|
g_upscaledTex = 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_bars != MenuBars::Black) && !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()));
|
|
}
|
|
|
|
const char* MenuScaleName() {
|
|
return g_menuScale == MenuScale::Integer ? "integer" : g_menuScale == MenuScale::Smooth ? "smooth" : "sharp";
|
|
}
|
|
|
|
// A render target that is also a texture (default pool): the texture keeps one reference, the level-0 surface another.
|
|
bool CreateRenderTexture(IDirect3DDevice9* dev, UINT w, UINT h, D3DFORMAT fmt, IDirect3DTexture9** tex, IDirect3DSurface9** surf) {
|
|
if (FAILED(dev->CreateTexture(w, h, 1, D3DUSAGE_RENDERTARGET, fmt, D3DPOOL_DEFAULT, tex, nullptr))) return false;
|
|
if (FAILED((*tex)->GetSurfaceLevel(0, surf))) {
|
|
(*tex)->Release();
|
|
*tex = nullptr;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// ---- MenuBars: the side bars filled with the menu picture ---------------------------------------------------------------
|
|
struct BarVertex {
|
|
float x, y, z, rhw;
|
|
DWORD color;
|
|
float u, v;
|
|
};
|
|
|
|
struct BarLook {
|
|
float brightness; // modulates the picture
|
|
int levels; // 0 = no blur (mirror); else the number of halvings (blur radius ~ 2^levels source pixels)
|
|
};
|
|
BarLook LookOf(MenuBars b) {
|
|
switch (b) {
|
|
case MenuBars::Blur: return {0.40f, 4};
|
|
case MenuBars::BlurLight: return {0.62f, 2};
|
|
default: return {0.45f, 0};
|
|
}
|
|
}
|
|
|
|
// The four rectangles around the fitted menu rectangle, covered by one textured draw. Blur: the cropped picture (aspect-fill:
|
|
// the monitor's aspect, centred, top/bottom cropped on a wide screen) in its smallest level over the whole back buffer.
|
|
// Mirror: the upscaled menu picture, mirrored (address mode) beyond its own edges.
|
|
bool DrawBars(IDirect3DDevice9* dev, IDirect3DSurface9* bb, const D3DSURFACE_DESC& desc, const RECT& d) {
|
|
const BarLook look = LookOf(g_bars);
|
|
const float mw = static_cast<float>(desc.Width), mh = static_cast<float>(desc.Height);
|
|
const float dw = static_cast<float>(d.right - d.left), dh = static_cast<float>(d.bottom - d.top);
|
|
IDirect3DTexture9* tex = nullptr;
|
|
if (look.levels > 0) {
|
|
if (!g_cornerValid || !g_corner) return false;
|
|
// aspect-fill crop in the game's picture
|
|
LONG cw = static_cast<LONG>(g_gameW), ch = static_cast<LONG>(g_gameH);
|
|
if (static_cast<LONGLONG>(desc.Width) * g_gameH > static_cast<LONGLONG>(desc.Height) * g_gameW) {
|
|
ch = static_cast<LONG>(static_cast<LONGLONG>(g_gameW) * desc.Height / desc.Width);
|
|
} else {
|
|
cw = static_cast<LONG>(static_cast<LONGLONG>(g_gameH) * desc.Width / desc.Height);
|
|
}
|
|
if (ch < 2) ch = 2;
|
|
if (cw < 2) cw = 2;
|
|
RECT crop{(static_cast<LONG>(g_gameW) - cw) / 2, (static_cast<LONG>(g_gameH) - ch) / 2, 0, 0};
|
|
crop.right = crop.left + cw;
|
|
crop.bottom = crop.top + ch;
|
|
if (g_levelCount != look.levels || g_levelCropW != static_cast<UINT>(cw) || g_levelCropH != static_cast<UINT>(ch)) {
|
|
ReleaseBars();
|
|
for (int i = 0; i < look.levels; ++i) {
|
|
BlurLevel& l = g_level[i];
|
|
l.w = static_cast<UINT>(cw) >> (i + 1);
|
|
l.h = static_cast<UINT>(ch) >> (i + 1);
|
|
if (l.w < 2) l.w = 2;
|
|
if (l.h < 2) l.h = 2;
|
|
if (!CreateRenderTexture(dev, l.w, l.h, desc.Format, &l.tex, &l.surf)) return false;
|
|
g_levelCount = i + 1;
|
|
}
|
|
g_levelCropW = cw;
|
|
g_levelCropH = ch;
|
|
}
|
|
if (FAILED(dev->StretchRect(g_corner, &crop, g_level[0].surf, nullptr, D3DTEXF_LINEAR))) return false;
|
|
for (int i = 1; i < look.levels; ++i)
|
|
if (FAILED(dev->StretchRect(g_level[i - 1].surf, nullptr, g_level[i].surf, nullptr, D3DTEXF_LINEAR))) return false;
|
|
tex = g_level[look.levels - 1].tex;
|
|
} else {
|
|
tex = g_upscaledTex;
|
|
if (!tex) return false;
|
|
}
|
|
if (!g_barState && FAILED(dev->CreateStateBlock(D3DSBT_ALL, &g_barState))) return false;
|
|
|
|
// (x, y) in back-buffer pixels -> texture coordinates
|
|
auto vertex = [&](float x, float y) {
|
|
BarVertex v{x - 0.5f, y - 0.5f, 0.0f, 1.0f, 0, 0, 0};
|
|
const DWORD g = static_cast<DWORD>(look.brightness * 255.0f + 0.5f);
|
|
v.color = D3DCOLOR_XRGB(g, g, g);
|
|
if (look.levels > 0) {
|
|
v.u = x / mw;
|
|
v.v = y / mh;
|
|
} else {
|
|
v.u = (x - d.left) / dw;
|
|
v.v = (y - d.top) / dh;
|
|
}
|
|
return v;
|
|
};
|
|
const RECT parts[4] = {{0, 0, static_cast<LONG>(desc.Width), d.top},
|
|
{0, d.bottom, static_cast<LONG>(desc.Width), static_cast<LONG>(desc.Height)},
|
|
{0, d.top, d.left, d.bottom},
|
|
{d.right, d.top, static_cast<LONG>(desc.Width), d.bottom}};
|
|
BarVertex verts[24];
|
|
int n = 0;
|
|
for (const RECT& r : parts) {
|
|
if (r.right <= r.left || r.bottom <= r.top) continue;
|
|
const float x0 = static_cast<float>(r.left), y0 = static_cast<float>(r.top), x1 = static_cast<float>(r.right),
|
|
y1 = static_cast<float>(r.bottom);
|
|
verts[n++] = vertex(x0, y0);
|
|
verts[n++] = vertex(x1, y0);
|
|
verts[n++] = vertex(x0, y1);
|
|
verts[n++] = vertex(x1, y0);
|
|
verts[n++] = vertex(x1, y1);
|
|
verts[n++] = vertex(x0, y1);
|
|
}
|
|
if (n == 0) return true;
|
|
|
|
IDirect3DSurface9* target = nullptr;
|
|
dev->GetRenderTarget(0, &target);
|
|
D3DVIEWPORT9 oldVp{};
|
|
dev->GetViewport(&oldVp);
|
|
g_barState->Capture();
|
|
if (target != bb) dev->SetRenderTarget(0, bb);
|
|
const D3DTEXTUREADDRESS address = look.levels > 0 ? D3DTADDRESS_CLAMP : D3DTADDRESS_MIRROR;
|
|
dev->SetRenderState(D3DRS_SEPARATEALPHABLENDENABLE, FALSE);
|
|
dev->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE);
|
|
dev->SetRenderState(D3DRS_ALPHATESTENABLE, FALSE);
|
|
dev->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE);
|
|
dev->SetRenderState(D3DRS_ZWRITEENABLE, FALSE);
|
|
dev->SetRenderState(D3DRS_STENCILENABLE, FALSE);
|
|
dev->SetRenderState(D3DRS_SCISSORTESTENABLE, FALSE);
|
|
dev->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
|
|
dev->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID);
|
|
dev->SetRenderState(D3DRS_FOGENABLE, FALSE);
|
|
dev->SetRenderState(D3DRS_LIGHTING, FALSE);
|
|
dev->SetRenderState(D3DRS_SPECULARENABLE, FALSE);
|
|
dev->SetRenderState(D3DRS_SRGBWRITEENABLE, FALSE);
|
|
dev->SetRenderState(D3DRS_COLORWRITEENABLE, 0xF);
|
|
dev->SetVertexShader(nullptr);
|
|
dev->SetPixelShader(nullptr);
|
|
dev->SetFVF(D3DFVF_XYZRHW | D3DFVF_DIFFUSE | D3DFVF_TEX1);
|
|
dev->SetTexture(0, tex);
|
|
dev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
|
|
dev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
|
|
dev->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
|
|
dev->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
|
|
dev->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE);
|
|
dev->SetTextureStageState(0, D3DTSS_TEXCOORDINDEX, 0);
|
|
dev->SetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_DISABLE);
|
|
dev->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
|
|
dev->SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
|
|
dev->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
|
|
dev->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
|
|
dev->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_NONE);
|
|
dev->SetSamplerState(0, D3DSAMP_ADDRESSU, address);
|
|
dev->SetSamplerState(0, D3DSAMP_ADDRESSV, address);
|
|
dev->SetSamplerState(0, D3DSAMP_SRGBTEXTURE, FALSE);
|
|
D3DVIEWPORT9 vp{0, 0, desc.Width, desc.Height, 0.0f, 1.0f};
|
|
dev->SetViewport(&vp);
|
|
bool ok = false;
|
|
if (SUCCEEDED(dev->BeginScene())) {
|
|
ok = SUCCEEDED(dev->DrawPrimitiveUP(D3DPT_TRIANGLELIST, n / 3, verts, sizeof(BarVertex)));
|
|
dev->EndScene();
|
|
}
|
|
g_barState->Apply();
|
|
dev->SetViewport(&oldVp);
|
|
if (target) {
|
|
if (target != bb) dev->SetRenderTarget(0, target);
|
|
target->Release();
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
// Cost of the bars, logged once after 300 frames when the environment variable DS2_MENUBARS_STATS is set: CPU time of the calls and
|
|
// GPU time between two timestamp queries around them (the queries are polled right away, which stalls the CPU - measurement only).
|
|
struct BarStats {
|
|
int frames = 0;
|
|
double cpuMs = 0, gpuMs = 0;
|
|
int gpuSamples = 0;
|
|
bool done = false;
|
|
int enabled = -1;
|
|
};
|
|
BarStats g_stats;
|
|
|
|
bool MakeQuery(IDirect3DDevice9* dev, D3DQUERYTYPE type, IDirect3DQuery9** q) { return *q || SUCCEEDED(dev->CreateQuery(type, q)); }
|
|
|
|
template <typename T>
|
|
bool PollQuery(IDirect3DQuery9* q, T* out) {
|
|
for (int i = 0; i < 2000000; ++i) {
|
|
HRESULT hr = q->GetData(out, sizeof(T), D3DGETDATA_FLUSH);
|
|
if (hr == S_OK) return true;
|
|
if (FAILED(hr)) return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool DrawBarsMeasured(IDirect3DDevice9* dev, IDirect3DSurface9* bb, const D3DSURFACE_DESC& desc, const RECT& d) {
|
|
if (g_stats.enabled < 0) {
|
|
char v[8] = "";
|
|
g_stats.enabled = GetEnvironmentVariableA("DS2_MENUBARS_STATS", v, sizeof(v)) > 0 ? (v[0] == 'c' ? 2 : 1) : 0; // "cpu": no GPU queries
|
|
}
|
|
if (!g_stats.enabled || g_stats.done) return DrawBars(dev, bb, desc, d);
|
|
bool gpu = g_stats.enabled == 1 && MakeQuery(dev, D3DQUERYTYPE_TIMESTAMPDISJOINT, &g_qDisjoint) && MakeQuery(dev, D3DQUERYTYPE_TIMESTAMP, &g_qStart) &&
|
|
MakeQuery(dev, D3DQUERYTYPE_TIMESTAMP, &g_qEnd) && MakeQuery(dev, D3DQUERYTYPE_TIMESTAMPFREQ, &g_qFreq);
|
|
LARGE_INTEGER f, t0, t1;
|
|
QueryPerformanceFrequency(&f);
|
|
if (gpu) {
|
|
g_qDisjoint->Issue(D3DISSUE_BEGIN);
|
|
g_qFreq->Issue(D3DISSUE_END);
|
|
g_qStart->Issue(D3DISSUE_END);
|
|
}
|
|
QueryPerformanceCounter(&t0);
|
|
bool ok = DrawBars(dev, bb, desc, d);
|
|
QueryPerformanceCounter(&t1);
|
|
g_stats.cpuMs += (t1.QuadPart - t0.QuadPart) * 1000.0 / f.QuadPart;
|
|
if (gpu) {
|
|
g_qEnd->Issue(D3DISSUE_END);
|
|
g_qDisjoint->Issue(D3DISSUE_END);
|
|
BOOL disjoint = TRUE;
|
|
UINT64 a = 0, b = 0, freq = 0;
|
|
if (PollQuery(g_qDisjoint, &disjoint) && !disjoint && PollQuery(g_qStart, &a) && PollQuery(g_qEnd, &b) && PollQuery(g_qFreq, &freq) &&
|
|
freq) {
|
|
g_stats.gpuMs += static_cast<double>(b - a) * 1000.0 / static_cast<double>(freq);
|
|
++g_stats.gpuSamples;
|
|
}
|
|
}
|
|
if (++g_stats.frames >= 300) {
|
|
g_stats.done = true;
|
|
ds2::log().Detail("display: MenuBars stats over %d frames: CPU %.3f ms/frame, GPU %.3f ms/frame (%d timestamp samples)", g_stats.frames,
|
|
g_stats.cpuMs / g_stats.frames, g_stats.gpuSamples ? g_stats.gpuMs / g_stats.gpuSamples : -1.0, g_stats.gpuSamples);
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
// 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), the bars around it.
|
|
// smooth (with MenuBars only): one step, a plain linear enlargement like the driver's stretch.
|
|
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 || g_menuScale == MenuScale::Smooth) k = 1;
|
|
UINT uw = g_gameW * k, uh = g_gameH * k;
|
|
HRESULT hr = S_OK;
|
|
const bool barsWanted = g_bars != MenuBars::Black && !g_barsFailed;
|
|
if (!g_upscaled || g_upW != uw || g_upH != uh || (barsWanted && !g_upscaledTex)) {
|
|
ReleaseUpscaled();
|
|
if (barsWanted) {
|
|
hr = CreateRenderTexture(dev, uw, uh, desc.Format, &g_upscaledTex, &g_upscaled) ? S_OK : E_FAIL;
|
|
} else {
|
|
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().Detail("display: menus %ux%u -> %ux%u (nearest) -> %ux%u (%s%s)", g_gameW, g_gameH, uw, uh, dw, dh, MenuScaleName(),
|
|
barsWanted ? ", MenuBars" : "");
|
|
}
|
|
}
|
|
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);
|
|
// Around the fitted rectangle (the game's corner may reach outside it): the picture-filled bars, else black. 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);
|
|
bool filled = false;
|
|
if (SUCCEEDED(hr) && barsWanted && (d.left > 0 || d.top > 0 || d.right < bw || d.bottom < bh)) {
|
|
filled = DrawBarsMeasured(dev, bb, desc, d);
|
|
if (!filled) {
|
|
ds2::log().Write("display: MenuBars failed, bars stay black");
|
|
g_barsFailed = true;
|
|
ReleaseBars();
|
|
}
|
|
}
|
|
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) && !filled && 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(),
|
|
MenuScaleName());
|
|
}
|
|
}
|
|
|
|
void OnBeforeCreate() {
|
|
ReleaseUpscaled(); // the game replaces its device (front end <-> world)
|
|
if (g_hudOn) widescreen::hud::BeforeCreate();
|
|
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().Detail("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().Detail("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().Detail("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().Detail("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().Detail("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);
|
|
const bool reuseDevice = ini.Bool("Display", "ReuseDevice", false); // the hub resets one device instead of replacing it (d3dhub.h)
|
|
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 menuBars[16];
|
|
ini.String("Display", "MenuBars", "blur", menuBars, sizeof(menuBars));
|
|
g_bars = _stricmp(menuBars, "blur") == 0 ? MenuBars::Blur
|
|
: _stricmp(menuBars, "blurlight") == 0 ? MenuBars::BlurLight
|
|
: _stricmp(menuBars, "mirror") == 0 ? MenuBars::Mirror
|
|
: MenuBars::Black;
|
|
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;
|
|
g_bars = MenuBars::Black;
|
|
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 || g_bars != MenuBars::Black);
|
|
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 || reuseDevice) {
|
|
const ds2::d3dhub::Api* hub = ds2::d3dhub::Connect("DS2Widescreen");
|
|
if (hub) {
|
|
if (!g_revalidate) hub->SetRevalidate(false);
|
|
if (reuseDevice) {
|
|
if (ds2::d3dhub::HasReuse(hub)) {
|
|
hub->SetReuseDevice(true);
|
|
ds2::log().Write("display: ReuseDevice=1 - the game's device is reset and handed out again instead of being replaced");
|
|
} else {
|
|
ds2::log().Write("SKIP display: ReuseDevice (the Direct3D hub of %s is older and has no device reuse)", hub->owner ? hub->owner : "another plugin");
|
|
}
|
|
}
|
|
ds2::d3dhub::Callbacks cb;
|
|
cb.name = "DS2Widescreen display";
|
|
cb.priority = ds2::d3dhub::kPrioHud;
|
|
if (menus) cb.AdjustParams = OnAdjustParams;
|
|
if (menus || hud || remixHooks || reuseDevice) {
|
|
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 || reuseDevice) ? 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.BindRadar = widescreen::hud::BindRadar;
|
|
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
|