DS2Revival/mods/widescreen/hud.cpp

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