DS2Revival/mods/widescreen/display.cpp
DS2Revival a1e2f8f9b5
Some checks failed
build / build (push) Failing after 1m35s
CI: build on the project's own runner at git.dsii.uk
2026-10-27 22:02:02 -04:00

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