496 lines
25 KiB
C++
496 lines
25 KiB
C++
// HUD scale ([Display] HudScale): the in-game interface is laid out 1:1 in screen pixels (anchors are pixel
|
|
// offsets from the screen edges), so on a 1440p or 4K monitor it is tiny. With HudScale=S (auto: world height
|
|
// / 720, 1 = off; a whole number or a decimal from 1.0 to 4.0, e.g. 1.5 for 1080p) in the borderless world:
|
|
// - UIShell resize (sub_73d500 / BW sub_741e50, thiscall(width, height); stores +0x78/+0x7c and re-anchors every
|
|
// window) is told the screen is W/S x H/S (rounded), so the HUD lays itself out for a smaller screen;
|
|
// - a transparent W/S x H/S "HUD layer" texture becomes render target 0 (cleared on first use in a frame,
|
|
// separate alpha blending: alpha accumulates as coverage, colour as premultiplied colour) for the parts of
|
|
// the interface pass (sub_4783cf / BW sub_480348, after the 3D world) that are laid out by the UI shell: the
|
|
// radar (its first call, +0x19) and UIShell::Draw (sub_7481c0 / BW sub_74cae0) up to Present. The names
|
|
// and health bars above heads, drawn in between from the 3D view, stay on the back buffer at full size;
|
|
// - our Present (display.cpp) composites that layer over the back buffer S times larger, blended "one, inverse
|
|
// source alpha". Whole-number scales use nearest-neighbour (crisp pixels, no smoothing). Other scales use
|
|
// "sharp bilinear" ([Display] HudFilter=sharp): the layer is first enlarged by ceil(S) with nearest-neighbour
|
|
// (every layer pixel is a hard block), then sampled down to the screen with bilinear filtering, so an edge is
|
|
// at most one screen pixel wide and nothing shimmers. HudFilter=smooth is plain bilinear, point plain nearest;
|
|
// - UIShell mouse moves (sub_745000 / BW sub_749950, thiscall(x, y): position at +0x90/+0x94 and the UI's hit
|
|
// testing) get the position divided by the scale. World picking does not go through the UI shell.
|
|
// The 3D view does not depend on the UI shell's size (tested: re-laying the UI out leaves the camera alone). The
|
|
// hardware cursor and the names/health bars above heads stay at their native size.
|
|
#include "../common/patch.h"
|
|
#include "../common/vtable_hook.h"
|
|
#include <d3d9.h>
|
|
#include <cstdlib>
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
#include <intrin.h>
|
|
#include <cstring>
|
|
|
|
namespace widescreen {
|
|
void WorldSize(int* w, int* h);
|
|
bool Borderless();
|
|
}
|
|
|
|
namespace {
|
|
|
|
enum class Filter { Sharp, Smooth, Point };
|
|
float g_setting = 0; // [Display] HudScale: 0 = auto
|
|
Filter g_filter = Filter::Sharp;
|
|
float g_scale = 1; // the scale in use while the HUD is scaled (world size / layout size)
|
|
float g_sx = 1, g_sy = 1; // the exact horizontal and vertical ratios (composite size / layer size): mouse, labels, loading screen
|
|
bool g_whole = true; // the scale is a whole number: nearest-neighbour blocks
|
|
bool g_active = false; // the UI shell currently has the divided size
|
|
bool g_pending = false; // the world's UI size was seen: divide it on the first world frame (ApplyScale)
|
|
int g_layerW = 0, g_layerH = 0; // the interface's layout size (the layer's size)
|
|
int g_fullW = 0, g_fullH = 0; // the size the shell had before (the world's size)
|
|
int g_dstW = 0, g_dstH = 0; // what the layer is composited into (the world's size; whole scales: layer x N)
|
|
|
|
IDirect3DDevice9* g_device = nullptr;
|
|
IDirect3DTexture9* g_layer = nullptr;
|
|
IDirect3DSurface9* g_layerSurface = nullptr;
|
|
IDirect3DTexture9* g_mid = nullptr; // sharp bilinear: the layer enlarged by ceil(scale), nearest-neighbour
|
|
IDirect3DSurface9* g_midSurface = nullptr;
|
|
bool g_midFailed = false;
|
|
IDirect3DStateBlock9* g_state = nullptr;
|
|
IDirect3DSurface9* g_loadCopy = nullptr; // the loading screen's own picture, kept while a scaled one is presented
|
|
bool g_loadScaled = false; // this frame's back buffer holds the scaled loading screen (restore after Present)
|
|
IDirect3DSurface9* g_savedTarget = nullptr; // non-null while the layer is bound
|
|
bool g_used = false; // the layer was cleared and drawn into this frame
|
|
bool g_failed = false; // hooks missing: never scale
|
|
bool g_layerFailed = false; // the layer could not be made on this device
|
|
bool g_relayout = false; // ...and the interface still has the divided size: undo at Present
|
|
void* g_shell = nullptr; // the UI shell last resized
|
|
|
|
void ReleaseLayer() {
|
|
if (g_loadCopy) g_loadCopy->Release(), g_loadCopy = nullptr;
|
|
g_loadScaled = false;
|
|
if (g_savedTarget) g_savedTarget->Release(), g_savedTarget = nullptr;
|
|
if (g_layerSurface) g_layerSurface->Release(), g_layerSurface = nullptr;
|
|
if (g_layer) g_layer->Release(), g_layer = nullptr;
|
|
if (g_midSurface) g_midSurface->Release(), g_midSurface = nullptr;
|
|
if (g_mid) g_mid->Release(), g_mid = nullptr;
|
|
if (g_state) g_state->Release(), g_state = nullptr;
|
|
g_used = false;
|
|
}
|
|
|
|
constexpr float kMinScale = 1.0f, kMaxScale = 4.0f;
|
|
|
|
// The scale for a world of this height. auto = height / 720: the interface gets a 720-line layout whatever the monitor
|
|
// (1080p: 1.5, 1200p: 1.67, 1440p: 2, 1600p: 2.22, 2160p: 3). Within 0.05 of a whole number it is snapped to it (so
|
|
// 1440p and 2160p stay crisp nearest-neighbour); below 1.2 (768p and the like) it stays off, because a 1.07x resample
|
|
// would only blur. No rounding to quarter steps: the layout height stays exactly 720.
|
|
float ScaleFor(int worldH) {
|
|
float s = g_setting;
|
|
if (s <= 0) {
|
|
s = static_cast<float>(worldH) / 720.0f;
|
|
if (s < 1.2f) return 1.0f;
|
|
float r = std::floor(s + 0.5f);
|
|
if (std::fabs(s - r) <= 0.05f) s = r;
|
|
}
|
|
if (s > kMaxScale) s = kMaxScale;
|
|
if (s < kMinScale) s = kMinScale;
|
|
float r = std::floor(s + 0.5f);
|
|
return std::fabs(s - r) < 0.001f ? r : s;
|
|
}
|
|
|
|
// Layout size and composite rectangle for a w x h world at scale s.
|
|
void Layout(int w, int h, float s) {
|
|
g_scale = s;
|
|
g_fullW = w;
|
|
g_fullH = h;
|
|
g_whole = std::floor(s) == s;
|
|
if (g_whole) { // exact blocks: the layer is the whole part of w/N, the composite N times that (a strip may stay uncovered)
|
|
int n = static_cast<int>(s);
|
|
g_layerW = w / n;
|
|
g_layerH = h / n;
|
|
g_dstW = g_layerW * n;
|
|
g_dstH = g_layerH * n;
|
|
} else {
|
|
g_layerW = static_cast<int>(std::floor(w / s + 0.5f));
|
|
g_layerH = static_cast<int>(std::floor(h / s + 0.5f));
|
|
g_dstW = w;
|
|
g_dstH = h;
|
|
}
|
|
g_sx = static_cast<float>(g_dstW) / static_cast<float>(g_layerW);
|
|
g_sy = static_cast<float>(g_dstH) / static_cast<float>(g_layerH);
|
|
}
|
|
|
|
// ---- UIShell resize -----------------------------------------------------------------------------
|
|
using Resize_t = void(__fastcall*)(void* shell, void* edx, int w, int h);
|
|
Resize_t g_resize;
|
|
|
|
void __fastcall HookResize(void* shell, void* edx, int w, int h) {
|
|
int mw = 0, mh = 0;
|
|
widescreen::WorldSize(&mw, &mh);
|
|
float n = ScaleFor(mh);
|
|
bool world = widescreen::Borderless() && w == mw && h == mh && n > 1.0f && !g_failed && !g_layerFailed;
|
|
// The game lays the interface out for the window size when it enters the world, and then shows its loading screen
|
|
// through drawing code of its own (not the UIShell::Draw hooked below: no layer, no composite). With the divided size
|
|
// that screen would be drawn small in the corner, so the division waits for the first frame of the world (ApplyScale).
|
|
if (world) {
|
|
Layout(w, h, n);
|
|
g_pending = true;
|
|
} else if (g_active || g_pending) {
|
|
ds2::log().Write("hud: interface at %dx%d (not scaled)", w, h);
|
|
}
|
|
g_active = false;
|
|
g_shell = shell;
|
|
g_relayout = false;
|
|
g_resize(shell, edx, w, h);
|
|
}
|
|
|
|
// First frame of the world interface pass (WorldFinished), before the item labels' projection is used by anything else: the
|
|
// interface gets the divided size.
|
|
void ApplyScale() {
|
|
if (!g_pending || !g_shell) return;
|
|
g_pending = false;
|
|
g_active = true;
|
|
ds2::log().Write("hud: interface laid out for %dx%d, drawn x%.3f (%s)", g_layerW, g_layerH, g_scale,
|
|
g_filter == Filter::Smooth ? "bilinear" : g_whole || g_filter == Filter::Point ? "nearest" : "sharp bilinear");
|
|
g_resize(g_shell, nullptr, g_layerW, g_layerH);
|
|
}
|
|
|
|
// ---- UIShell mouse -------------------------------------------------------------------------------
|
|
using Mouse_t = void(__fastcall*)(void* shell, void* edx, int x, int y);
|
|
Mouse_t g_mouse;
|
|
|
|
void __fastcall HookMouse(void* shell, void* edx, int x, int y) {
|
|
if (g_active) { // the layer pixel that contains the centre of the screen pixel
|
|
x = static_cast<int>(std::floor((x + 0.5f) / g_sx));
|
|
y = static_cast<int>(std::floor((y + 0.5f) / g_sy));
|
|
}
|
|
g_mouse(shell, edx, x, y);
|
|
}
|
|
|
|
// ---- UIShell::Draw: bind the HUD layer -------------------------------------------------------------
|
|
using Draw_t = void(__fastcall*)(void* shell, void* edx);
|
|
Draw_t g_draw;
|
|
|
|
// While the layer is bound, the game's own "back to the back buffer" (SetRenderTarget(0, back buffer) after a
|
|
// render-to-texture pass such as the overlay map's 512x512 map texture) must land on the layer too, or what it draws
|
|
// next goes straight to the back buffer at interface pixels, unscaled and under the composite.
|
|
using SetRenderTarget_t = HRESULT(STDMETHODCALLTYPE*)(IDirect3DDevice9*, DWORD, IDirect3DSurface9*);
|
|
HRESULT STDMETHODCALLTYPE HookSetRenderTarget(IDirect3DDevice9* dev, DWORD index, IDirect3DSurface9* surface);
|
|
ds2::VtableHook g_rtHook(37, reinterpret_cast<void*>(HookSetRenderTarget));
|
|
bool g_rtBypass = false; // our own SetRenderTarget calls
|
|
|
|
HRESULT STDMETHODCALLTYPE HookSetRenderTarget(IDirect3DDevice9* dev, DWORD index, IDirect3DSurface9* surface) {
|
|
if (!g_rtBypass && index == 0 && g_savedTarget && g_layerSurface && surface == g_savedTarget) surface = g_layerSurface;
|
|
return g_rtHook.Original<SetRenderTarget_t>(dev)(dev, index, surface);
|
|
}
|
|
|
|
// The layer texture and the state block the composite uses.
|
|
HRESULT MakeLayer(IDirect3DDevice9* dev) {
|
|
HRESULT hr = S_OK;
|
|
bool created = !g_layer;
|
|
if (!g_layer) hr = dev->CreateTexture(g_layerW, g_layerH, 1, D3DUSAGE_RENDERTARGET, D3DFMT_A8R8G8B8, D3DPOOL_DEFAULT, &g_layer, nullptr);
|
|
if (SUCCEEDED(hr) && !g_layerSurface) hr = g_layer->GetSurfaceLevel(0, &g_layerSurface);
|
|
if (SUCCEEDED(hr) && !g_state) hr = dev->CreateStateBlock(D3DSBT_ALL, &g_state);
|
|
if (SUCCEEDED(hr) && created) ds2::log().Write("hud: %dx%d layer, composited x%.3f", g_layerW, g_layerH, g_scale);
|
|
return hr;
|
|
}
|
|
|
|
// What the layer is composited from, and with which filter. Whole scales: the layer, nearest-neighbour. Other scales with
|
|
// HudFilter=sharp: the layer enlarged by ceil(scale) with nearest-neighbour (g_mid), sampled bilinear. Smooth: the layer,
|
|
// bilinear. If the enlarged copy cannot be made, the plain bilinear stands in.
|
|
void Source(IDirect3DDevice9* dev, IDirect3DTexture9** tex, IDirect3DSurface9** surf, D3DTEXTUREFILTERTYPE* filter) {
|
|
*tex = g_layer;
|
|
*surf = g_layerSurface;
|
|
*filter = g_filter == Filter::Smooth ? D3DTEXF_LINEAR : (g_whole || g_filter == Filter::Point) ? D3DTEXF_POINT : D3DTEXF_LINEAR;
|
|
if (g_whole || g_filter != Filter::Sharp || g_midFailed) return;
|
|
int k = static_cast<int>(std::ceil(std::max(g_sx, g_sy) - 0.001f));
|
|
HRESULT hr = S_OK;
|
|
if (!g_mid) hr = dev->CreateTexture(g_layerW * k, g_layerH * k, 1, D3DUSAGE_RENDERTARGET, D3DFMT_A8R8G8B8, D3DPOOL_DEFAULT, &g_mid, nullptr);
|
|
if (SUCCEEDED(hr) && !g_midSurface) hr = g_mid->GetSurfaceLevel(0, &g_midSurface);
|
|
if (SUCCEEDED(hr)) hr = dev->StretchRect(g_layerSurface, nullptr, g_midSurface, nullptr, D3DTEXF_POINT);
|
|
if (FAILED(hr)) {
|
|
ds2::log().Write("hud: sharp bilinear step x%d failed (0x%08lX), using plain bilinear", k, static_cast<unsigned long>(hr));
|
|
g_midFailed = true;
|
|
if (g_midSurface) g_midSurface->Release(), g_midSurface = nullptr;
|
|
if (g_mid) g_mid->Release(), g_mid = nullptr;
|
|
return;
|
|
}
|
|
*tex = g_mid;
|
|
*surf = g_midSurface;
|
|
}
|
|
|
|
bool BindLayer() {
|
|
IDirect3DDevice9* dev = g_device;
|
|
if (!g_active || g_failed || g_layerFailed || !dev || g_savedTarget) return g_savedTarget != nullptr;
|
|
HRESULT hr = MakeLayer(dev);
|
|
if (SUCCEEDED(hr)) hr = dev->GetRenderTarget(0, &g_savedTarget);
|
|
if (SUCCEEDED(hr)) { // the layer is single-sampled: it cannot share a multisampled device's depth buffer
|
|
D3DSURFACE_DESC desc{};
|
|
g_savedTarget->GetDesc(&desc);
|
|
if (desc.MultiSampleType != D3DMULTISAMPLE_NONE) hr = D3DERR_INVALIDCALL;
|
|
}
|
|
if (SUCCEEDED(hr)) {
|
|
g_rtBypass = true;
|
|
hr = dev->SetRenderTarget(0, g_layerSurface);
|
|
g_rtBypass = false;
|
|
}
|
|
if (SUCCEEDED(hr) && !g_used) hr = dev->Clear(0, nullptr, D3DCLEAR_TARGET, 0x00000000, 1.0f, 0);
|
|
if (FAILED(hr)) {
|
|
ds2::log().Write("hud: layer failed (0x%08lX), HUD scaling off", static_cast<unsigned long>(hr));
|
|
if (g_savedTarget) {
|
|
g_rtBypass = true;
|
|
dev->SetRenderTarget(0, g_savedTarget);
|
|
g_rtBypass = false;
|
|
}
|
|
ReleaseLayer();
|
|
g_layerFailed = true;
|
|
g_relayout = true; // the interface goes back to full size at Present (not in the middle of drawing it)
|
|
return false;
|
|
}
|
|
// Colour blends as the game asks; alpha accumulates coverage (one, inverse source alpha), so the layer holds
|
|
// premultiplied colour for the composite. The game never sets these states itself.
|
|
dev->SetRenderState(D3DRS_SEPARATEALPHABLENDENABLE, TRUE);
|
|
dev->SetRenderState(D3DRS_SRCBLENDALPHA, D3DBLEND_ONE);
|
|
dev->SetRenderState(D3DRS_DESTBLENDALPHA, D3DBLEND_INVSRCALPHA);
|
|
g_used = true;
|
|
return true;
|
|
}
|
|
|
|
// Back to the render target the game had (this also resets the viewport to it).
|
|
void UnbindLayer() {
|
|
if (!g_savedTarget) return;
|
|
g_rtBypass = true;
|
|
g_device->SetRenderTarget(0, g_savedTarget);
|
|
g_rtBypass = false;
|
|
g_savedTarget->Release();
|
|
g_savedTarget = nullptr;
|
|
g_device->SetRenderState(D3DRS_SEPARATEALPHABLENDENABLE, FALSE);
|
|
}
|
|
|
|
void __fastcall HookDraw(void* shell, void* edx) {
|
|
BindLayer(); // stays bound until Present
|
|
g_draw(shell, edx);
|
|
}
|
|
|
|
// Item labels (UIItemOverlay, run from the interface pass before the radar) are UI windows: the game projects each
|
|
// item to the screen with the renderer (full screen pixels), then places the window inside the UI shell's screen
|
|
// (+0x78/+0x7c), which HudScale has made N times smaller. Unscaled projections land outside it: the labels are hidden or
|
|
// pile up at the edge (the game's collision avoidance pushes them into view). The projection call there gets its result
|
|
// divided by N, so the label is placed where the item is on the scaled interface.
|
|
using Project_t = float*(__fastcall*)(float* pos, void* edx, float* out);
|
|
Project_t g_project;
|
|
|
|
float* __fastcall HookLabelProject(float* pos, void* edx, float* out) {
|
|
float* r = g_project(pos, edx, out);
|
|
if (g_active && g_scale > 1.0f) {
|
|
out[0] /= g_sx;
|
|
out[1] /= g_sy;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
struct LayerVertex {
|
|
float x, y, z, rhw, u, v;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
// The HUD scale for other plugins: DS2HudScale is the whole number nearest to it (1 = none), DS2HudScaleF the exact value,
|
|
// DS2HudInfo the horizontal and vertical ratios and the layout size (returns 0 and 1/1/0/0 while the HUD is not scaled).
|
|
#pragma comment(linker, "/EXPORT:DS2HudScale=_DS2HudScale@0")
|
|
#pragma comment(linker, "/EXPORT:DS2HudScaleF=_DS2HudScaleF@0")
|
|
#pragma comment(linker, "/EXPORT:DS2HudInfo=_DS2HudInfo@16")
|
|
extern "C" int __stdcall DS2HudScale() { return g_active ? static_cast<int>(std::floor(g_scale + 0.5f)) : 1; }
|
|
extern "C" float __stdcall DS2HudScaleF() { return g_active ? g_scale : 1.0f; }
|
|
extern "C" int __stdcall DS2HudInfo(float* sx, float* sy, int* layerW, int* layerH) {
|
|
if (sx) *sx = g_active ? g_sx : 1.0f;
|
|
if (sy) *sy = g_active ? g_sy : 1.0f;
|
|
if (layerW) *layerW = g_active ? g_layerW : 0;
|
|
if (layerH) *layerH = g_active ? g_layerH : 0;
|
|
return g_active ? 1 : 0;
|
|
}
|
|
|
|
namespace widescreen::hud {
|
|
|
|
bool InstallBwPortraitShift(); // portrait.cpp
|
|
|
|
|
|
bool Wanted(const ds2::Ini& ini) {
|
|
char v[16];
|
|
ini.String("Display", "HudScale", "auto", v, sizeof(v));
|
|
g_setting = _stricmp(v, "auto") == 0 ? 0.0f : static_cast<float>(std::atof(v)); // not a number: auto
|
|
if (!(g_setting > 0.0f)) g_setting = 0.0f;
|
|
ini.String("Display", "HudFilter", "sharp", v, sizeof(v));
|
|
g_filter = _stricmp(v, "smooth") == 0 ? Filter::Smooth : _stricmp(v, "point") == 0 ? Filter::Point : Filter::Sharp;
|
|
return !(g_setting > 0.0f && g_setting < 1.001f); // 1 (or less) = off
|
|
}
|
|
|
|
// The HUD scale hooks. The world-finished point (ApplyScale, the layer around the radar) and the Present composite are the
|
|
// shared hub's (display.cpp registers WorldFinished / BindInterface / Present); call this only when the hub has installed
|
|
// them. False = not installed (the callbacks do nothing without the shell hooks).
|
|
bool Install() {
|
|
InstallBwPortraitShift(); // Broken World only; the UI shell is about to shrink
|
|
struct Site {
|
|
const char* pattern;
|
|
const char* what;
|
|
void* hook;
|
|
void** original;
|
|
} sites[] = {
|
|
{"55 8B EC 83 EC 0C 8B 55 0C 53 8B D9 8B 43 78 8B 4B 7C 57 89 45 F8 8B 45 08 52 89 4D FC 50 8B CB E8",
|
|
"hud: UIShell resize", reinterpret_cast<void*>(HookResize), reinterpret_cast<void**>(&g_resize)},
|
|
{"55 8B EC 83 EC 0C ?? ?? ?? ?? ?? 53 56 8B D9 8B 88 4C 02 00 00 8B 81 C0 06 00 00 8B 10 6A 08 6A 17 50",
|
|
"hud: UIShell::Draw", reinterpret_cast<void*>(HookDraw), reinterpret_cast<void**>(&g_draw)},
|
|
{"55 8B EC 56 8B F1 8B 86 D4 00 00 00 85 C0 75 15 8B 86 D8 00 00 00 85 C0 75 0B FF 86 18 01 00 00 5E 5D C2 08 00",
|
|
"hud: UIShell mouse", reinterpret_cast<void*>(HookMouse), reinterpret_cast<void**>(&g_mouse)},
|
|
};
|
|
DWORD found[3] = {};
|
|
for (int i = 0; i < 3; ++i) {
|
|
found[i] = ds2::FindUnique(sites[i].pattern);
|
|
if (!found[i]) {
|
|
ds2::log().Write("SKIP hud: %s not found, HUD not scaled", sites[i].what);
|
|
return false;
|
|
}
|
|
}
|
|
// "...fadd [..]; fld; fadd st(1); fstp; fstp st(0); call <world to screen>; fld [out+4]; push 0; call __ftol2" in the
|
|
// item label update, identical in both exes.
|
|
DWORD proj = ds2::FindUnique("8D 85 38 FF FF FF 50 D8 45 80 D8 05 ?? ?? ?? ?? D9 45 D8 D8 C1 D9 5D D8 DD D8 E8 ?? ?? ?? ?? D9 85 3C FF FF FF 6A 00 E8");
|
|
if (!proj) ds2::log().Write("SKIP hud: item label projection not found, item labels keep full-screen positions");
|
|
if (proj) {
|
|
DWORD psite = proj + 26, ptarget = psite + 5 + *reinterpret_cast<const LONG*>(psite + 1);
|
|
g_project = reinterpret_cast<Project_t>(ptarget);
|
|
ds2::RedirectCall(psite, ptarget, reinterpret_cast<DWORD>(&HookLabelProject), "hud: item label projection (scaled interface)");
|
|
}
|
|
for (int i = 0; i < 3; ++i) {
|
|
const BYTE* code = reinterpret_cast<const BYTE*>(found[i]);
|
|
BYTE expect[6];
|
|
std::memcpy(expect, code, 6);
|
|
*sites[i].original = ds2::Detour(found[i], expect, 6, sites[i].hook, sites[i].what);
|
|
if (!*sites[i].original) {
|
|
ds2::log().Write("SKIP hud: could not hook %s", sites[i].what);
|
|
g_failed = true;
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Hub stage WorldFinished (priority 0, before the anti-aliasing): the first frame of the world divides the interface's size.
|
|
void WorldFinished(IDirect3DDevice9*) { ApplyScale(); }
|
|
|
|
// Hub stage BindInterface / UnbindInterface: the radar of the interface pass is drawn into the HUD layer.
|
|
bool BindInterface(IDirect3DDevice9*) { return BindLayer(); }
|
|
void UnbindInterface() { UnbindLayer(); }
|
|
|
|
void DeviceCreated(IDirect3DDevice9* dev) {
|
|
ReleaseLayer();
|
|
g_device = dev;
|
|
g_rtHook.Install(dev);
|
|
g_layerFailed = false; // a new device may well manage it; the game lays its interface out again for it
|
|
}
|
|
|
|
void BeforeReset() { ReleaseLayer(); }
|
|
|
|
// Once per frame (hub Frame stage): put the render-target hook back into the device's current table.
|
|
void Revalidate(IDirect3DDevice9* dev) {
|
|
if (dev && dev == g_device && g_rtHook.Installed()) g_rtHook.Install(dev);
|
|
}
|
|
|
|
// From our Present: if the layer was bound this frame, put the back buffer back and draw the layer over it.
|
|
// The loading screen is drawn by code of its own straight onto the back buffer, laid out (centred) for the full-size shell:
|
|
// a small panel in the middle of a huge black screen. The middle W/N x H/N of it is enlarged N times (nearest neighbour),
|
|
// like the rest of the interface; the game's own picture is put back after Present (the loading code draws only what changes).
|
|
void ScaleLoadingFrame(IDirect3DDevice9* dev) {
|
|
if (!g_pending || dev != g_device || g_savedTarget || g_failed || g_layerFailed || g_scale <= 1.0f) return;
|
|
IDirect3DSurface9* bb = nullptr;
|
|
if (FAILED(dev->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &bb))) return;
|
|
D3DSURFACE_DESC desc{};
|
|
bb->GetDesc(&desc);
|
|
HRESULT hr = desc.MultiSampleType == D3DMULTISAMPLE_NONE ? MakeLayer(dev) : D3DERR_INVALIDCALL;
|
|
if (SUCCEEDED(hr) && !g_loadCopy) hr = dev->CreateRenderTarget(desc.Width, desc.Height, desc.Format, D3DMULTISAMPLE_NONE, 0, FALSE, &g_loadCopy, nullptr);
|
|
LONG cx = (static_cast<LONG>(desc.Width) - g_layerW) / 2, cy = (static_cast<LONG>(desc.Height) - g_layerH) / 2;
|
|
RECT mid{cx, cy, cx + g_layerW, cy + g_layerH};
|
|
if (SUCCEEDED(hr) && cx >= 0 && cy >= 0) {
|
|
hr = dev->StretchRect(bb, nullptr, g_loadCopy, nullptr, D3DTEXF_POINT);
|
|
if (SUCCEEDED(hr)) hr = dev->StretchRect(bb, &mid, g_layerSurface, nullptr, D3DTEXF_POINT);
|
|
if (SUCCEEDED(hr)) {
|
|
IDirect3DTexture9* tex;
|
|
IDirect3DSurface9* src;
|
|
D3DTEXTUREFILTERTYPE filter;
|
|
Source(dev, &tex, &src, &filter);
|
|
hr = dev->StretchRect(src, nullptr, bb, nullptr, filter);
|
|
}
|
|
g_loadScaled = SUCCEEDED(hr);
|
|
}
|
|
bb->Release();
|
|
}
|
|
|
|
void AfterPresent(IDirect3DDevice9* dev) {
|
|
if (!g_loadScaled || !g_loadCopy || dev != g_device) return;
|
|
g_loadScaled = false;
|
|
IDirect3DSurface9* bb = nullptr;
|
|
if (FAILED(dev->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &bb))) return;
|
|
dev->StretchRect(g_loadCopy, nullptr, bb, nullptr, D3DTEXF_POINT);
|
|
bb->Release();
|
|
}
|
|
|
|
void BeforePresent(IDirect3DDevice9* dev) {
|
|
ScaleLoadingFrame(dev);
|
|
if (g_relayout && g_active && g_shell) { // the layer failed: full-size interface, unscaled mouse
|
|
ds2::log().Write("hud: interface back to %dx%d", g_fullW, g_fullH);
|
|
g_active = false;
|
|
g_relayout = false;
|
|
g_resize(g_shell, nullptr, g_fullW, g_fullH);
|
|
}
|
|
if (dev != g_device || !g_used) return;
|
|
UnbindLayer(); // also resets the viewport to the whole back buffer...
|
|
g_state->Capture(); // ...which is what the restored state keeps
|
|
g_used = false;
|
|
IDirect3DSurface9* target = nullptr;
|
|
if (FAILED(dev->GetRenderTarget(0, &target))) return;
|
|
D3DSURFACE_DESC desc{};
|
|
target->GetDesc(&desc);
|
|
target->Release();
|
|
float w = static_cast<float>(g_dstW), h = static_cast<float>(g_dstH);
|
|
IDirect3DTexture9* tex;
|
|
IDirect3DSurface9* src;
|
|
D3DTEXTUREFILTERTYPE filter;
|
|
Source(dev, &tex, &src, &filter); // (a StretchRect: outside the state block, before the scene)
|
|
LayerVertex quad[4] = {{-0.5f, -0.5f, 0, 1, 0, 0}, {w - 0.5f, -0.5f, 0, 1, 1, 0}, {-0.5f, h - 0.5f, 0, 1, 0, 1},
|
|
{w - 0.5f, h - 0.5f, 0, 1, 1, 1}};
|
|
dev->SetRenderState(D3DRS_SEPARATEALPHABLENDENABLE, FALSE);
|
|
dev->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
|
|
dev->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ONE);
|
|
dev->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
|
|
dev->SetRenderState(D3DRS_BLENDOP, D3DBLENDOP_ADD);
|
|
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_FOGENABLE, FALSE);
|
|
dev->SetRenderState(D3DRS_LIGHTING, FALSE);
|
|
dev->SetRenderState(D3DRS_COLORWRITEENABLE, 0xF);
|
|
dev->SetVertexShader(nullptr);
|
|
dev->SetPixelShader(nullptr);
|
|
dev->SetFVF(D3DFVF_XYZRHW | D3DFVF_TEX1);
|
|
dev->SetTexture(0, tex);
|
|
dev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
|
|
dev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
|
|
dev->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
|
|
dev->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);
|
|
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, filter);
|
|
dev->SetSamplerState(0, D3DSAMP_MAGFILTER, filter);
|
|
dev->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_NONE);
|
|
dev->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_CLAMP);
|
|
dev->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_CLAMP);
|
|
D3DVIEWPORT9 vp{0, 0, desc.Width, desc.Height, 0.0f, 1.0f};
|
|
dev->SetViewport(&vp);
|
|
if (SUCCEEDED(dev->BeginScene())) {
|
|
dev->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, quad, sizeof(LayerVertex));
|
|
dev->EndScene();
|
|
}
|
|
g_state->Apply();
|
|
dev->SetRenderState(D3DRS_SEPARATEALPHABLENDENABLE, FALSE);
|
|
}
|
|
|
|
} // namespace widescreen::hud
|