mirror of
https://github.com/sal063/AC6_recomp
synced 2026-08-21 23:00:53 -04:00
Merge pull request #32 from Dipshet/add-ultrawide
Add ultrawide support: hor+ in missions
This commit is contained in:
@@ -42,6 +42,7 @@ set(AC6RECOMP_SOURCES
|
||||
src/ac6_backend_fixes/ac6_backend_pass_classifier.cpp
|
||||
src/ac6_backend_fixes/ac6_fps_physics_fix.cpp
|
||||
src/ac6_backend_fixes/ac6_kbm_input.cpp
|
||||
src/ac6_backend_fixes/ac6_widescreen.cpp
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
|
||||
@@ -0,0 +1,956 @@
|
||||
// AC6 enhancement: arbitrary aspect ratio (ultrawide) - camera aspect patcher.
|
||||
//
|
||||
// Discovery (exchange/ultrawide/ac6recomp.log, 2026-07-03): the game keeps the
|
||||
// display aspect ratio 16:9 as 1.7777778f (big-endian 0x3FE38E39) per camera
|
||||
// object, laid out around the aspect field as
|
||||
// [-0x04] fov (~0.40 / 0.44 / 0.68 observed; changes with zoom)
|
||||
// [+0x00] aspect 1.7777778f
|
||||
// [+0x04] near (0.1 / 1.0 observed)
|
||||
// [+0x08] far (24000.0 observed)
|
||||
// with the camera's view rotation basis a few rows below. A global camera
|
||||
// template lives at guest 0x82A160C8, live cameras on the physically-backed
|
||||
// heap, and the game's static 16:9 default constant sits in a data table in
|
||||
// the XEX image at 0x8206A0F4 (and 0x9206A0F4 through the second view).
|
||||
//
|
||||
// Mechanism: a background thread polls the game's mode-task state machine
|
||||
// (the runtime-verified chain [0x8293B930] -> +0x8 -> vtable, see
|
||||
// docs/re/subsystems/selftest_macro.md) every 250 ms and, on the 2 s cadence
|
||||
// or immediately on a mission transition,
|
||||
// 1. (re)patches the static default at fixed addresses, so cameras created
|
||||
// afterwards are born with the current target aspect, and
|
||||
// 2. signature-scans committed guest memory for camera objects carrying the
|
||||
// previous aspect and pokes their aspect field.
|
||||
// INSIDE a mission (mode task CModeTaskGame: gameplay, in-engine cutscenes,
|
||||
// pause) the target is the wide aspect - the game builds its own wider
|
||||
// projection, the compressed 16:9 guest output is presented stretched to the
|
||||
// window (via the presenter's letterbox override), and the draw-time UI
|
||||
// shrink keeps the 2D layer proportioned. OUTSIDE a mission everything is reverted to 16:9 and
|
||||
// the presenter is forced to letterbox: menus, hangar, briefing, FMV and the
|
||||
// attract demo render exactly vanilla.
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#include <native/ui/presenter.h>
|
||||
#include <rex/cvar.h>
|
||||
#include <rex/logging.h>
|
||||
#include <rex/system/xmemory.h>
|
||||
|
||||
#include "../render_hooks.h"
|
||||
#include "ac6_widescreen.h"
|
||||
|
||||
REXCVAR_DEFINE_BOOL(ac6_widescreen, false, "AC6",
|
||||
"Arbitrary aspect ratio (hor+), in-mission only. The target aspect "
|
||||
"is derived from the actual window size (re-checked continuously; "
|
||||
"a live resize adapts). While the game's mode task is the mission "
|
||||
"(gameplay, in-engine cutscenes, pause), cameras are patched from "
|
||||
"16:9 to the window aspect, the presenter fills the window, and "
|
||||
"the 2D layer is pre-squeezed at draw time for a crisp 16:9 HUD "
|
||||
"(the game-placed target-marker shader stays full-width by design; "
|
||||
"its art is squared by narrowing the geometry). Everywhere else "
|
||||
"(menus, hangar, briefing, FMV, attract) and at 16:9-or-narrower "
|
||||
"windows, cameras stay 16:9 and the presentation letterboxes - "
|
||||
"vanilla. While enabled the feature drives the presenter's "
|
||||
"letterbox decision itself; it never writes the present_letterbox "
|
||||
"cvar, so nothing leaks into a saved config.");
|
||||
REXCVAR_DEFINE_BOOL(ac6_widescreen_cinematics, true, "AC6",
|
||||
"With ac6_widescreen: render in-engine cinematics wide too, even "
|
||||
"outside the mission mode task (detected via the demo-manager "
|
||||
"cinematic signal, the same one the cutscene frame-rate clamp "
|
||||
"uses - so ac6_cutscene_clamp=false disables this detection). "
|
||||
"Cinematics are staged and framed for 16:9, so widening can "
|
||||
"expose things outside that frame (set edges, actors popping in "
|
||||
"at the frustum border). Off = cinematics letterbox at 16:9 "
|
||||
"as staged.");
|
||||
// Read (never written) only to report it in the activation log.
|
||||
REXCVAR_DECLARE(bool, present_letterbox);
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kNativeAspect = 1.7777778f; // BE 0x3FE38E39, exactly what the game stores
|
||||
constexpr uint64_t kGuestScanEnd = 0xC0000000ull; // C0/E0 views alias A0 - skip them
|
||||
// Mode poll every 250 ms (a cheap 3-dereference guest read) so mission
|
||||
// transitions re-aim the cameras promptly; the full memory sweep still runs on
|
||||
// the 2 s cadence (8 polls) or immediately on a transition.
|
||||
constexpr DWORD kModePollMs = 250;
|
||||
constexpr uint32_t kSweepEveryPolls = 8;
|
||||
|
||||
// The game's front end is a mode-task state machine (docs/re/subsystems/
|
||||
// selftest_macro.md, runtime-verified): [0x8293B930] -> CTaskModeManager
|
||||
// singleton, +0x8 -> the currently-running CModeTask, +0x0 -> its vtable
|
||||
// pointer, which is a static per-class address and therefore a screen id.
|
||||
// 0x820642F4 = CModeTaskGame, the in-mission task (gameplay, in-engine
|
||||
// cutscenes and the pause menu all run under it). The +0x8 slot is briefly
|
||||
// null while the manager swaps tasks - treated as "hold the previous state".
|
||||
constexpr uint32_t kModeManagerPtrEA = 0x8293B930;
|
||||
constexpr uint32_t kModeTaskSlotOffset = 0x8;
|
||||
constexpr uint32_t kModeTaskGameVtable = 0x820642F4;
|
||||
|
||||
std::atomic<rex::memory::Memory*> g_ws_memory{nullptr};
|
||||
// Bits of the UI X-shrink factor (16:9 / target aspect), published by the
|
||||
// patcher thread for the per-draw ortho patch; 0 = UI patching disabled.
|
||||
std::atomic<uint32_t> g_ui_shrink_bits{0};
|
||||
// Whether the current scene should render/present wide: the mode task is the
|
||||
// mission, or an in-engine cinematic is playing with
|
||||
// ac6_widescreen_cinematics on. Published by the patcher thread's poll.
|
||||
// Drives BOTH halves of the policy: the presentation (fill when wide,
|
||||
// letterbox everywhere else) and the camera aspect target (wide vs native).
|
||||
std::atomic<bool> g_wide_scene{false};
|
||||
// Whether a valid WIDER-than-16:9 target is currently in effect (published by
|
||||
// the patcher thread). False at 16:9 and at NARROWER windows - there the
|
||||
// presentation must letterbox even in-mission, or the 16:9 guest frame would
|
||||
// stretch vertically to fill a narrow window. (Narrower-than-16:9 rendering
|
||||
// itself is not supported: the world could widen vertically by the same
|
||||
// camera mechanism, but the 1280x720 UI cannot be expanded horizontally
|
||||
// without pushing corner-anchored elements off-screen.)
|
||||
std::atomic<bool> g_target_wide{false};
|
||||
|
||||
// Whether the UI shrink applies to the CURRENT scene (shared by the
|
||||
// constant-level patch and the sub-viewport rect shrink; must stay in sync
|
||||
// with the apply logic in WidescreenPatchUiOrtho). Wide scenes only: during
|
||||
// world rendering (the crisp-HUD config) and in-mission without world draws
|
||||
// (the pause menu over the frozen frame); outside wide scenes everything
|
||||
// presents letterboxed vanilla and nothing is shrunk.
|
||||
bool UiShrinkSceneActive() {
|
||||
return g_wide_scene.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// The target-marker vertex shader (guest ucode hash) is never UI-shrunk: the
|
||||
// game places markers by projecting world coordinates through the widened
|
||||
// camera, so they are already positioned for the full-width display -
|
||||
// shrinking them would pull them off-target toward screen center. Their box
|
||||
// graphics render proportionally wider instead; positions are exact.
|
||||
constexpr uint64_t kMarkerVsUcodeHash = 0xB686E181ACD543E9ull;
|
||||
|
||||
// Per-swap "already narrowed" bookkeeping for the marker-quad fix. The game
|
||||
// rotates three ~692 KB vertex arenas; 16384 vertices covers one at the
|
||||
// observed 52-byte stride with room to spare. Command-processor thread only.
|
||||
constexpr uint32_t kMarkerMaxVertices = 16384;
|
||||
constexpr uint32_t kMarkerBitmapWords = kMarkerMaxVertices / 64;
|
||||
|
||||
struct MarkerArenaGuard {
|
||||
uint32_t base = 0;
|
||||
uint64_t bits[kMarkerBitmapWords] = {};
|
||||
|
||||
// Returns true if the vertex was already narrowed this swap.
|
||||
bool TestAndSet(uint32_t vertex_index) {
|
||||
if (vertex_index >= kMarkerMaxVertices) {
|
||||
return true; // Out of range: treat as done, i.e. leave it alone.
|
||||
}
|
||||
uint64_t& word = bits[vertex_index >> 6];
|
||||
uint64_t bit = uint64_t(1) << (vertex_index & 63);
|
||||
bool was_set = (word & bit) != 0;
|
||||
word |= bit;
|
||||
return was_set;
|
||||
}
|
||||
|
||||
void Clear() { std::memset(bits, 0, sizeof(bits)); }
|
||||
};
|
||||
|
||||
MarkerArenaGuard g_marker_guards[4];
|
||||
|
||||
MarkerArenaGuard& MarkerGuardFor(uint32_t arena_base) {
|
||||
for (MarkerArenaGuard& g : g_marker_guards) {
|
||||
if (g.base == arena_base) {
|
||||
return g;
|
||||
}
|
||||
}
|
||||
// Claim a free slot, or recycle the last one (the game uses three arenas).
|
||||
for (MarkerArenaGuard& g : g_marker_guards) {
|
||||
if (g.base == 0) {
|
||||
g.base = arena_base;
|
||||
g.Clear();
|
||||
return g;
|
||||
}
|
||||
}
|
||||
MarkerArenaGuard& g = g_marker_guards[rex::countof(g_marker_guards) - 1];
|
||||
g.base = arena_base;
|
||||
g.Clear();
|
||||
return g;
|
||||
}
|
||||
|
||||
void MarkerGuardsResetForSwap() {
|
||||
for (MarkerArenaGuard& g : g_marker_guards) {
|
||||
g.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t HostBitsOf(float value) {
|
||||
uint32_t bits;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
return _byteswap_ulong(bits); // little-endian dword whose bytes read big-endian
|
||||
}
|
||||
|
||||
// SEH-safe single-word accessors for the poke paths. Note the SDK's vectored
|
||||
// handler runs BEFORE these __except filters, so a write fault on a
|
||||
// GPU-write-watched physical page is still recovered transparently (like any
|
||||
// guest write); only genuinely unrecoverable access violations land here and
|
||||
// are reported as failure instead of crashing the process.
|
||||
bool SafeReadU32(const uint32_t* p, uint32_t* out) noexcept {
|
||||
__try {
|
||||
*out = *p;
|
||||
return true;
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool SafeWriteU32(uint32_t* p, uint32_t value) noexcept {
|
||||
__try {
|
||||
*p = value;
|
||||
return true;
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Big-endian guest dword read through the translated view, SEH-safe.
|
||||
bool SafeReadGuestU32(rex::memory::Memory* memory, uint32_t guest_ea, uint32_t* out) {
|
||||
if (!guest_ea) {
|
||||
return false;
|
||||
}
|
||||
uint32_t raw;
|
||||
if (!SafeReadU32(memory->TranslateVirtual<const uint32_t*>(guest_ea), &raw)) {
|
||||
return false;
|
||||
}
|
||||
*out = _byteswap_ulong(raw);
|
||||
return true;
|
||||
}
|
||||
|
||||
// The current mode task's vtable pointer (= screen id), or 0 while unknown
|
||||
// (manager not up yet, slot mid-swap, or the read faulted).
|
||||
uint32_t ReadCurrentScreenId(rex::memory::Memory* memory) {
|
||||
uint32_t manager, task, vtable;
|
||||
if (!SafeReadGuestU32(memory, kModeManagerPtrEA, &manager) || !manager) {
|
||||
return 0;
|
||||
}
|
||||
if (!SafeReadGuestU32(memory, manager + kModeTaskSlotOffset, &task) || !task) {
|
||||
return 0;
|
||||
}
|
||||
if (!SafeReadGuestU32(memory, task, &vtable)) {
|
||||
return 0;
|
||||
}
|
||||
return vtable;
|
||||
}
|
||||
|
||||
// The raw signature scan, SEH-guarded against pages vanishing mid-read.
|
||||
// Scalar-only frame so __try is legal. Finds dwords equal to the big-endian
|
||||
// 16:9 aspect whose neighbors look like a camera (fov, near, far in sane
|
||||
// ranges); returns match count, stores up to max_out dword indices.
|
||||
size_t WideScanRegionRaw(const uint32_t* words, size_t count, uint32_t aspect_pattern,
|
||||
uint32_t prev_pattern, uint32_t* out_indices,
|
||||
size_t max_out) noexcept {
|
||||
size_t n = 0;
|
||||
__try {
|
||||
for (size_t i = 1; i + 2 < count; ++i) {
|
||||
if (words[i] != aspect_pattern && (!prev_pattern || words[i] != prev_pattern)) {
|
||||
continue;
|
||||
}
|
||||
uint32_t w;
|
||||
float fov, near_plane, far_plane;
|
||||
w = _byteswap_ulong(words[i - 1]);
|
||||
std::memcpy(&fov, &w, sizeof(fov));
|
||||
w = _byteswap_ulong(words[i + 1]);
|
||||
std::memcpy(&near_plane, &w, sizeof(near_plane));
|
||||
w = _byteswap_ulong(words[i + 2]);
|
||||
std::memcpy(&far_plane, &w, sizeof(far_plane));
|
||||
if (fov > 0.05f && fov < 2.0f && near_plane > 0.005f && near_plane < 10.0f &&
|
||||
far_plane > 1000.0f && far_plane < 200000.0f) {
|
||||
if (n < max_out) {
|
||||
out_indices[n] = uint32_t(i);
|
||||
}
|
||||
++n;
|
||||
}
|
||||
}
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// The previously applied target's big-endian pattern (0 = none) - lets the
|
||||
// static defaults be retargeted when the auto-derived aspect changes (window
|
||||
// resized mid-session).
|
||||
uint32_t g_prev_target_bits = 0; // Patcher thread only.
|
||||
|
||||
// The game's static 16:9 default aspect constant in the XEX image (and its
|
||||
// second-view alias): cameras copy their initial aspect from here.
|
||||
constexpr uint32_t kStaticDefaultAddrs[] = {0x8206A0F4u, 0x9206A0F4u};
|
||||
|
||||
// Keep the game's static 16:9 default constant(s) patched. Re-checked every
|
||||
// sweep in case the game rewrites them (e.g. applying video settings).
|
||||
void PatchStaticDefaults(rex::memory::Memory* memory, uint32_t aspect_pattern,
|
||||
uint32_t target_bits, float target) {
|
||||
static uint32_t patch_logs = 0;
|
||||
static uint32_t unexpected_logs = 0;
|
||||
for (uint32_t guest : kStaticDefaultAddrs) {
|
||||
uint32_t* host = memory->TranslateVirtual<uint32_t*>(guest);
|
||||
// Early in boot the page may not be committed yet (this runs from ~2s
|
||||
// after graphics init, during the startup logo) - skip and retry on the
|
||||
// next sweep rather than touching it.
|
||||
MEMORY_BASIC_INFORMATION mbi{};
|
||||
if (!VirtualQuery(host, &mbi, sizeof(mbi)) || mbi.State != MEM_COMMIT ||
|
||||
(mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD))) {
|
||||
continue;
|
||||
}
|
||||
uint32_t cur;
|
||||
if (!SafeReadU32(host, &cur)) {
|
||||
continue;
|
||||
}
|
||||
if (cur == target_bits) {
|
||||
continue; // Already patched.
|
||||
}
|
||||
bool retarget = g_prev_target_bits && cur == g_prev_target_bits;
|
||||
if (cur != aspect_pattern && !retarget) {
|
||||
// Not the value we expect - wrong address for this build/version, or
|
||||
// the game stores something else here right now. Don't touch it.
|
||||
if (unexpected_logs < 4) {
|
||||
++unexpected_logs;
|
||||
REXLOG_ERROR("[AC6-WIDE] static default @ 0x{:08X}: unexpected 0x{:08X} "
|
||||
"(expected 16:9), skipping",
|
||||
guest, _byteswap_ulong(cur));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// The XEX image copy may be mapped read-only - unprotect before writing
|
||||
// (kept writable; this field is re-checked every sweep anyway).
|
||||
bool need_unprotect =
|
||||
!(mbi.Protect & (PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READWRITE |
|
||||
PAGE_EXECUTE_WRITECOPY));
|
||||
if (need_unprotect) {
|
||||
DWORD old_protect;
|
||||
if (!VirtualProtect(host, sizeof(uint32_t), PAGE_READWRITE, &old_protect)) {
|
||||
if (unexpected_logs < 4) {
|
||||
++unexpected_logs;
|
||||
REXLOG_ERROR("[AC6-WIDE] static default @ 0x{:08X}: read-only and unprotect "
|
||||
"failed, skipping",
|
||||
guest);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (SafeWriteU32(host, target_bits) && patch_logs < 8) {
|
||||
++patch_logs;
|
||||
REXLOG_ERROR("[AC6-WIDE] static default @ 0x{:08X}: 1.77778 -> {:g}{}", guest, target,
|
||||
need_unprotect ? " (page unprotected)" : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Signature-scan committed guest memory for camera objects whose aspect field
|
||||
// matches match_a (or match_b, 0 = unused) and poke it to to_bits. Returns the
|
||||
// number of fields patched. Used in both directions: native -> wide entering a
|
||||
// mission (plus stale previous-wide values after a window resize), and
|
||||
// wide -> native leaving one.
|
||||
uint32_t WidescreenSweep(rex::memory::Memory* memory, uint32_t match_a, uint32_t match_b,
|
||||
uint32_t to_bits, float to_value) {
|
||||
// One-time heartbeat pair: if the process dies between these two lines, the
|
||||
// log pinpoints the sweep as the culprit.
|
||||
static bool first_sweep = true;
|
||||
if (first_sweep) {
|
||||
REXLOG_ERROR("[AC6-WIDE] first sweep starting");
|
||||
}
|
||||
|
||||
static uint32_t poke_logs = 0;
|
||||
static uint32_t sweep_logs = 0;
|
||||
static uint32_t skipped_ro_logs = 0;
|
||||
constexpr size_t kMaxRegionMatches = 64;
|
||||
uint32_t indices[kMaxRegionMatches];
|
||||
uint32_t patched = 0;
|
||||
|
||||
uint64_t guest = 0x00010000;
|
||||
while (guest < kGuestScanEnd) {
|
||||
uint8_t* host = memory->TranslateVirtual(uint32_t(guest));
|
||||
MEMORY_BASIC_INFORMATION mbi{};
|
||||
if (!VirtualQuery(host, &mbi, sizeof(mbi)) || !mbi.RegionSize) {
|
||||
break;
|
||||
}
|
||||
uint64_t skip = uint64_t(host - static_cast<uint8_t*>(mbi.BaseAddress));
|
||||
uint64_t len = std::min(uint64_t(mbi.RegionSize) - skip, kGuestScanEnd - guest);
|
||||
if (!len) {
|
||||
break;
|
||||
}
|
||||
bool readable =
|
||||
mbi.State == MEM_COMMIT && !(mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) &&
|
||||
(mbi.Protect & (PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READ |
|
||||
PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY));
|
||||
bool writable = (mbi.Protect & (PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READWRITE |
|
||||
PAGE_EXECUTE_WRITECOPY)) != 0;
|
||||
if (readable) {
|
||||
size_t found = WideScanRegionRaw(reinterpret_cast<const uint32_t*>(host), size_t(len / 4),
|
||||
match_a, match_b, indices, kMaxRegionMatches);
|
||||
size_t stored = std::min(found, kMaxRegionMatches);
|
||||
for (size_t h = 0; h < stored; ++h) {
|
||||
uint32_t hit_guest = uint32_t(guest + uint64_t(indices[h]) * 4);
|
||||
// Physical-view pages (>= 0xA0000000) may be read-only due to the
|
||||
// SDK's GPU write watching; the poke faults and the SDK's handler
|
||||
// recovers it like any guest write. A read-only page in a plain
|
||||
// virtual heap has no such recovery - skip those.
|
||||
if (!writable && hit_guest < 0xA0000000u) {
|
||||
if (skipped_ro_logs < 4) {
|
||||
++skipped_ro_logs;
|
||||
REXLOG_ERROR("[AC6-WIDE] camera @ 0x{:08X} in read-only region, skipping",
|
||||
hit_guest);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
uint32_t* field = reinterpret_cast<uint32_t*>(host + uint64_t(indices[h]) * 4);
|
||||
uint32_t fov_word = 0;
|
||||
SafeReadU32(field - 1, &fov_word);
|
||||
uint32_t w = _byteswap_ulong(fov_word);
|
||||
float fov;
|
||||
std::memcpy(&fov, &w, sizeof(fov));
|
||||
if (!SafeWriteU32(field, to_bits)) {
|
||||
continue;
|
||||
}
|
||||
++patched;
|
||||
if (poke_logs < 32) {
|
||||
++poke_logs;
|
||||
REXLOG_ERROR("[AC6-WIDE] camera aspect @ 0x{:08X} (fov={:g}) -> {:g}", hit_guest,
|
||||
fov, to_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
guest += len;
|
||||
}
|
||||
if (first_sweep) {
|
||||
first_sweep = false;
|
||||
REXLOG_ERROR("[AC6-WIDE] first sweep done");
|
||||
}
|
||||
if (patched && sweep_logs < 16) {
|
||||
++sweep_logs;
|
||||
REXLOG_ERROR("[AC6-WIDE] sweep: patched {} camera aspect field(s) -> {:g}", patched,
|
||||
to_value);
|
||||
}
|
||||
return patched;
|
||||
}
|
||||
|
||||
DWORD WINAPI WidescreenThread(LPVOID) {
|
||||
const uint32_t native_bits = HostBitsOf(kNativeAspect);
|
||||
uint32_t poll_count = 0;
|
||||
bool last_in_mission = false;
|
||||
bool last_wide_scene = false;
|
||||
uint32_t last_screen_id = 0;
|
||||
// The wide pattern last written anywhere (0 = never widened). Kept across
|
||||
// reverts so widen sweeps also convert stale leftovers.
|
||||
uint32_t applied_wide_bits = 0;
|
||||
// Consecutive sweeps that patched nothing since the last transition; out of
|
||||
// the mission the scan stops after two clean passes (the statics recheck is
|
||||
// cheap and continues) so the front end is not scanned forever.
|
||||
uint32_t clean_reverts = 0;
|
||||
for (;;) {
|
||||
Sleep(kModePollMs);
|
||||
bool enabled = REXCVAR_GET(ac6_widescreen);
|
||||
rex::memory::Memory* memory = g_ws_memory.load(std::memory_order_acquire);
|
||||
// Mode poll, every cycle: cheap SEH-safe 3-dereference guest read. A null
|
||||
// read (manager not up, task slot mid-swap) holds the previous state.
|
||||
bool in_mission = last_in_mission;
|
||||
if (enabled && memory) {
|
||||
uint32_t id = ReadCurrentScreenId(memory);
|
||||
if (id != 0) {
|
||||
in_mission = (id == kModeTaskGameVtable);
|
||||
if (id != last_screen_id) {
|
||||
last_screen_id = id;
|
||||
static uint32_t mode_logs = 0;
|
||||
if (mode_logs < 32) {
|
||||
++mode_logs;
|
||||
// The id log exists so an unlisted mode task that SHOULD present
|
||||
// wide (if some in-mission path swaps tasks) can be identified
|
||||
// from a user log and whitelisted.
|
||||
REXLOG_ERROR("[AC6-WIDE] mode task 0x{:08X} ({})", id,
|
||||
in_mission ? "mission" : "front-end");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
in_mission = false;
|
||||
}
|
||||
last_in_mission = in_mission;
|
||||
// Opt-in: in-engine cinematics outside the mission task (story scenes in
|
||||
// the campaign flow) render wide too. Same demo-manager signal as the
|
||||
// cutscene frame-rate clamp, ~300 ms decay - the 250 ms poll tracks it.
|
||||
bool wide_scene =
|
||||
in_mission || (enabled && REXCVAR_GET(ac6_widescreen_cinematics) &&
|
||||
ac6::IsCinematicActive());
|
||||
g_wide_scene.store(wide_scene, std::memory_order_relaxed);
|
||||
bool transition = wide_scene != last_wide_scene;
|
||||
last_wide_scene = wide_scene;
|
||||
++poll_count;
|
||||
if (!transition && poll_count < kSweepEveryPolls) {
|
||||
continue; // Sweep on the 2 s cadence or immediately on a transition.
|
||||
}
|
||||
poll_count = 0;
|
||||
// The target aspect is the actual window's, re-derived each sweep (a live
|
||||
// resize adapts). At or narrower than 16:9 the widening disables (clamped
|
||||
// to native -> target invalid -> letterboxed presentation).
|
||||
float target = 0.0f;
|
||||
if (enabled) {
|
||||
uint32_t surface_w, surface_h;
|
||||
if (rex::ui::GetPresentSurfaceSize(&surface_w, &surface_h) && surface_h) {
|
||||
target = float(surface_w) / float(surface_h);
|
||||
if (target > 8.0f) {
|
||||
target = 8.0f;
|
||||
}
|
||||
if (target < kNativeAspect) {
|
||||
target = kNativeAspect;
|
||||
}
|
||||
static float last_logged_target = 0.0f;
|
||||
static uint32_t auto_logs = 0;
|
||||
if (std::fabs(target - last_logged_target) > 1e-3f && auto_logs < 8) {
|
||||
++auto_logs;
|
||||
last_logged_target = target;
|
||||
REXLOG_ERROR("[AC6-WIDE] auto aspect: window {}x{} -> target {:g}", surface_w,
|
||||
surface_h, target);
|
||||
}
|
||||
}
|
||||
// No surface yet (very early boot): retry next sweep.
|
||||
}
|
||||
bool target_valid =
|
||||
target > 0.5f && target < 8.0f && std::fabs(target - kNativeAspect) >= 1e-4f;
|
||||
g_target_wide.store(enabled && target_valid, std::memory_order_relaxed);
|
||||
// Publish the UI shrink factor for the per-draw ortho patch (0 = off).
|
||||
// Scene gating (mission / world) happens at the consumers.
|
||||
uint32_t shrink_bits = 0;
|
||||
if (enabled && target_valid) {
|
||||
float shrink = kNativeAspect / target;
|
||||
std::memcpy(&shrink_bits, &shrink, sizeof(shrink_bits));
|
||||
}
|
||||
g_ui_shrink_bits.store(shrink_bits, std::memory_order_relaxed);
|
||||
if (!enabled || !memory) {
|
||||
continue;
|
||||
}
|
||||
bool widen = wide_scene && target_valid;
|
||||
// Any change in what the cameras should be aimed at re-arms the sweep
|
||||
// (scene transitions AND target flips, e.g. a mid-mission resize to or
|
||||
// from a <=16:9 window).
|
||||
static bool last_widen = false;
|
||||
if (widen != last_widen) {
|
||||
clean_reverts = 0;
|
||||
}
|
||||
last_widen = widen;
|
||||
if (widen) {
|
||||
// Entering / inside a wide scene (mission, or an opted-in cinematic):
|
||||
// keep the static defaults patched (so cameras are born wide) and
|
||||
// convert any native or stale-wide cameras.
|
||||
uint32_t to_bits = HostBitsOf(target);
|
||||
if (to_bits != applied_wide_bits) {
|
||||
clean_reverts = 0;
|
||||
}
|
||||
PatchStaticDefaults(memory, native_bits, to_bits, target);
|
||||
uint32_t stale =
|
||||
(applied_wide_bits && applied_wide_bits != to_bits) ? applied_wide_bits : 0;
|
||||
WidescreenSweep(memory, native_bits, stale, to_bits, target);
|
||||
applied_wide_bits = to_bits;
|
||||
g_prev_target_bits = to_bits;
|
||||
} else if (applied_wide_bits) {
|
||||
// Out of every wide scene (or the target became native, e.g. a live
|
||||
// resize to 16:9 or narrower): restore the static defaults and revert
|
||||
// wide cameras so everything renders vanilla 16:9.
|
||||
PatchStaticDefaults(memory, native_bits, native_bits, kNativeAspect);
|
||||
if (clean_reverts < 2) {
|
||||
uint32_t patched =
|
||||
WidescreenSweep(memory, applied_wide_bits, 0, native_bits, kNativeAspect);
|
||||
clean_reverts = patched ? 0 : clean_reverts + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace ac6 {
|
||||
|
||||
void WidescreenInit(rex::memory::Memory* memory) {
|
||||
if (!memory) {
|
||||
return;
|
||||
}
|
||||
g_ws_memory.store(memory, std::memory_order_release);
|
||||
static std::once_flag once;
|
||||
std::call_once(once, [] {
|
||||
// No cvar is written here, deliberately. The feature drives presentation
|
||||
// through the presenter's letterbox OVERRIDE instead (see
|
||||
// WidescreenNotifySwapSource): writing present_letterbox would leak into
|
||||
// the user's saved config - the in-game settings menu persists current
|
||||
// cvar values - and would then keep the game stretched after the feature
|
||||
// was switched off again.
|
||||
if (REXCVAR_GET(ac6_widescreen)) {
|
||||
// present_letterbox is REPORTED here, never written: it must still read
|
||||
// exactly what the user configured (default true) with the feature on,
|
||||
// so an in-game settings save can never persist a value we chose.
|
||||
REXLOG_ERROR("[AC6-WIDE] widescreen active: cinematics={} (present_letterbox cvar "
|
||||
"untouched at {}; presentation driven by the override)",
|
||||
REXCVAR_GET(ac6_widescreen_cinematics) ? 1 : 0,
|
||||
REXCVAR_GET(present_letterbox) ? 1 : 0);
|
||||
}
|
||||
CreateThread(nullptr, 0, WidescreenThread, nullptr, 0, nullptr);
|
||||
});
|
||||
}
|
||||
|
||||
bool WidescreenPatchUiOrtho(uint32_t* vs_float_constants, uint64_t vs_ucode_hash,
|
||||
bool sub_viewport) {
|
||||
uint32_t shrink_bits = g_ui_shrink_bits.load(std::memory_order_relaxed);
|
||||
if (!shrink_bits) {
|
||||
return false;
|
||||
}
|
||||
float shrink;
|
||||
std::memcpy(&shrink, &shrink_bits, sizeof(shrink));
|
||||
bool world = ac6::WorldRenderActiveRecently();
|
||||
float* c = reinterpret_cast<float*>(vs_float_constants);
|
||||
// Scene logic (shared helper): wide scenes only - the shrink exists to
|
||||
// cancel the fill-window stretch, which is active exactly there.
|
||||
bool apply = UiShrinkSceneActive();
|
||||
// Sub-viewport draws (radar window, PiP inset) are placed by their
|
||||
// VIEWPORT - the viewport rect is shrunk instead (WidescreenViewportShrinkX
|
||||
// consumed in UpdateFixedFunctionState); their constants stay untouched.
|
||||
// World scenes only, matching the viewport-shrink gate: menu/hangar
|
||||
// sub-viewport panels keep their ordinary constant-level treatment.
|
||||
if (sub_viewport && world) {
|
||||
apply = false;
|
||||
}
|
||||
// The game-placed target markers stay full-width (see kMarkerVsUcodeHash).
|
||||
if (vs_ucode_hash == kMarkerVsUcodeHash) {
|
||||
apply = false;
|
||||
}
|
||||
// Runs on the command processor thread only.
|
||||
static uint32_t patch_logs = 0;
|
||||
// Idempotency ring: (a, tx) bit patterns this patch has produced. The
|
||||
// register file persists across draws, and unlike the old exact-value
|
||||
// match, the generalized shape rule would re-match its own output and
|
||||
// compound the shrink every draw that reuses stale constants - so anything
|
||||
// we ever emitted is recognized and skipped.
|
||||
static uint64_t shrunk_keys[256] = {};
|
||||
static uint32_t shrunk_key_count = 0;
|
||||
static uint32_t shrunk_key_next = 0;
|
||||
bool patched = false;
|
||||
// Generalized screen-space 2D transform detection: any 4-vec4 block shaped
|
||||
// r0 = (m00, m01, 0, tx) r1 = (m10, m11, 0, ty)
|
||||
// r2 = (0, 0, c, tz) r3 = (0, 0, 0, 1) exactly
|
||||
// with a tiny 2x2 (all |m| < 0.05 - screen transforms are 2/width-sized;
|
||||
// excludes identity, world matrices and perspective/billboard blocks, whose
|
||||
// w row is never (0,0,0,1)). Rotation is allowed - the radar map/blips spin
|
||||
// with heading. Covers the plain 1280x720 UI ortho AND composed 2D
|
||||
// transforms (radar contents, PiP window), so nested elements shrink
|
||||
// consistently with their frames. Scaling the whole X output row (m00, m01,
|
||||
// tx) shrinks around NDC 0 = screen center. The matrix needs 4 vec4s
|
||||
// starting at n, so scan c0..c60.
|
||||
for (uint32_t n = 0; n <= 60; ++n) {
|
||||
float* r0 = c + 4 * n;
|
||||
const float* r1 = r0 + 4;
|
||||
const float* r2 = r0 + 8;
|
||||
const float* r3 = r0 + 12;
|
||||
if (r3[0] != 0.0f || r3[1] != 0.0f || r3[2] != 0.0f || r3[3] != 1.0f) {
|
||||
continue;
|
||||
}
|
||||
if (r0[2] != 0.0f || r1[2] != 0.0f || r2[0] != 0.0f || r2[1] != 0.0f) {
|
||||
continue;
|
||||
}
|
||||
float m00_abs = std::fabs(r0[0]);
|
||||
float m01_abs = std::fabs(r0[1]);
|
||||
float m10_abs = std::fabs(r1[0]);
|
||||
float m11_abs = std::fabs(r1[1]);
|
||||
float x_row_max = m00_abs > m01_abs ? m00_abs : m01_abs;
|
||||
float y_row_max = m10_abs > m11_abs ? m10_abs : m11_abs;
|
||||
float all_max = x_row_max > y_row_max ? x_row_max : y_row_max;
|
||||
if (!(all_max < 0.05f && x_row_max > 1e-7f && y_row_max > 1e-7f)) {
|
||||
continue;
|
||||
}
|
||||
float* tx = &r0[3];
|
||||
// +-8: composed small-scale transforms overshoot +-1 considerably (the
|
||||
// PiP window quad sits at ty ~ 5.8).
|
||||
if (*tx < -8.0f || *tx > 8.0f || r1[3] < -8.0f || r1[3] > 8.0f) {
|
||||
continue;
|
||||
}
|
||||
// Skip transforms this patch already shrank (see the ring above): hash
|
||||
// the X output row we mutate (m00, m01, tx).
|
||||
uint32_t m00_bits, m01_bits, tx_bits;
|
||||
std::memcpy(&m00_bits, &r0[0], sizeof(m00_bits));
|
||||
std::memcpy(&m01_bits, &r0[1], sizeof(m01_bits));
|
||||
std::memcpy(&tx_bits, tx, sizeof(tx_bits));
|
||||
uint64_t key = 1469598103934665603ull;
|
||||
key = (key ^ m00_bits) * 1099511628211ull;
|
||||
key = (key ^ m01_bits) * 1099511628211ull;
|
||||
key = (key ^ tx_bits) * 1099511628211ull;
|
||||
bool already_shrunk = false;
|
||||
for (uint32_t i = 0; i < shrunk_key_count; ++i) {
|
||||
if (shrunk_keys[i] == key) {
|
||||
already_shrunk = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (already_shrunk || !apply) {
|
||||
continue;
|
||||
}
|
||||
r0[0] *= shrink;
|
||||
r0[1] *= shrink;
|
||||
*tx *= shrink;
|
||||
patched = true;
|
||||
// Remember the shrunk output so it is never shrunk again.
|
||||
std::memcpy(&m00_bits, &r0[0], sizeof(m00_bits));
|
||||
std::memcpy(&m01_bits, &r0[1], sizeof(m01_bits));
|
||||
std::memcpy(&tx_bits, tx, sizeof(tx_bits));
|
||||
key = 1469598103934665603ull;
|
||||
key = (key ^ m00_bits) * 1099511628211ull;
|
||||
key = (key ^ m01_bits) * 1099511628211ull;
|
||||
key = (key ^ tx_bits) * 1099511628211ull;
|
||||
shrunk_keys[shrunk_key_next] = key;
|
||||
shrunk_key_next = (shrunk_key_next + 1) & 255;
|
||||
if (shrunk_key_count < 256) {
|
||||
++shrunk_key_count;
|
||||
}
|
||||
if (patch_logs < 8) {
|
||||
++patch_logs;
|
||||
REXLOG_ERROR("[AC6-WIDE] screen transform @ c{} (m00={:g} tx={:g}) shrunk x{:g}", n,
|
||||
r0[0] / shrink, *tx / shrink, shrink);
|
||||
}
|
||||
}
|
||||
return patched;
|
||||
}
|
||||
|
||||
bool WidescreenWantsMarkerQuadFix(uint64_t vs_ucode_hash) {
|
||||
return vs_ucode_hash == kMarkerVsUcodeHash &&
|
||||
g_ui_shrink_bits.load(std::memory_order_relaxed) != 0 &&
|
||||
g_wide_scene.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void WidescreenShrinkMarkerQuads(uint8_t* vertices, uint32_t vertex_stride, uint32_t pos_offset,
|
||||
const uint8_t* indices, bool indices_32bit, uint32_t count,
|
||||
uint32_t arena_base) {
|
||||
uint32_t shrink_bits = g_ui_shrink_bits.load(std::memory_order_relaxed);
|
||||
if (!vertices || !vertex_stride || count < 4 || !shrink_bits) {
|
||||
return;
|
||||
}
|
||||
float shrink;
|
||||
std::memcpy(&shrink, &shrink_bits, sizeof(shrink));
|
||||
if (!(shrink > 0.0f) || shrink >= 0.999f) {
|
||||
return;
|
||||
}
|
||||
MarkerArenaGuard& guard = MarkerGuardFor(arena_base);
|
||||
uint32_t quads = count / 4;
|
||||
if (!quads) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read every quad's bounds up front. Text is drawn one quad per GLYPH, so a
|
||||
// quad is not an element: narrowing each glyph about its own centre leaves
|
||||
// the string's letter spacing at full width (letters end up thin and spread
|
||||
// out). Elements are recovered below by grouping.
|
||||
struct QuadBounds {
|
||||
uint32_t vi[4];
|
||||
float xlo, xhi, ylo, yhi;
|
||||
bool valid;
|
||||
};
|
||||
static std::vector<QuadBounds> bounds; // CP thread only; reused per draw.
|
||||
bounds.clear();
|
||||
bounds.reserve(quads);
|
||||
for (uint32_t q = 0; q < quads; ++q) {
|
||||
QuadBounds b{};
|
||||
b.valid = true;
|
||||
float x[4], y[4];
|
||||
for (uint32_t c = 0; c < 4 && b.valid; ++c) {
|
||||
uint32_t at = q * 4 + c;
|
||||
uint32_t vi;
|
||||
if (!indices) {
|
||||
vi = at;
|
||||
} else if (indices_32bit) {
|
||||
uint32_t raw;
|
||||
std::memcpy(&raw, indices + at * 4, sizeof(raw));
|
||||
vi = _byteswap_ulong(raw);
|
||||
} else {
|
||||
uint16_t raw;
|
||||
std::memcpy(&raw, indices + at * 2, sizeof(raw));
|
||||
vi = _byteswap_ushort(raw);
|
||||
}
|
||||
if (vi >= kMarkerMaxVertices) {
|
||||
b.valid = false;
|
||||
break;
|
||||
}
|
||||
b.vi[c] = vi;
|
||||
const uint8_t* vp = vertices + size_t(vi) * vertex_stride + pos_offset;
|
||||
uint32_t raw_x, raw_y;
|
||||
std::memcpy(&raw_x, vp, sizeof(raw_x));
|
||||
std::memcpy(&raw_y, vp + sizeof(float), sizeof(raw_y));
|
||||
raw_x = _byteswap_ulong(raw_x);
|
||||
raw_y = _byteswap_ulong(raw_y);
|
||||
std::memcpy(&x[c], &raw_x, sizeof(x[c]));
|
||||
std::memcpy(&y[c], &raw_y, sizeof(y[c]));
|
||||
if (!std::isfinite(x[c]) || !std::isfinite(y[c])) {
|
||||
b.valid = false;
|
||||
}
|
||||
}
|
||||
if (b.valid) {
|
||||
b.xlo = b.xhi = x[0];
|
||||
b.ylo = b.yhi = y[0];
|
||||
for (uint32_t c = 1; c < 4; ++c) {
|
||||
b.xlo = x[c] < b.xlo ? x[c] : b.xlo;
|
||||
b.xhi = x[c] > b.xhi ? x[c] : b.xhi;
|
||||
b.ylo = y[c] < b.ylo ? y[c] : b.ylo;
|
||||
b.yhi = y[c] > b.yhi ? y[c] : b.yhi;
|
||||
}
|
||||
}
|
||||
bounds.push_back(b);
|
||||
}
|
||||
|
||||
// Group consecutive quads into elements. The game emits a string's glyphs
|
||||
// back to back, on one baseline, with a small kerning gap (measured: 8 px
|
||||
// glyphs on a 10 px pitch, i.e. 2 px gaps), so a run of quads sharing a Y
|
||||
// span and separated by less than kElementGapPx is one element. Everything
|
||||
// else stays its own element, including a lone box quad.
|
||||
constexpr float kElementGapPx = 6.0f;
|
||||
constexpr float kBaselineEpsPx = 1.0f;
|
||||
struct Element {
|
||||
uint32_t first, last;
|
||||
float xlo, xhi;
|
||||
};
|
||||
static std::vector<Element> elements; // CP thread only; reused per draw.
|
||||
elements.clear();
|
||||
for (uint32_t q = 0; q < quads;) {
|
||||
if (!bounds[q].valid) {
|
||||
++q;
|
||||
continue;
|
||||
}
|
||||
Element e{};
|
||||
e.first = e.last = q;
|
||||
e.xlo = bounds[q].xlo;
|
||||
e.xhi = bounds[q].xhi;
|
||||
const float line_ylo = bounds[q].ylo;
|
||||
const float line_yhi = bounds[q].yhi;
|
||||
for (uint32_t n = q + 1; n < quads; ++n) {
|
||||
const QuadBounds& b = bounds[n];
|
||||
if (!b.valid) {
|
||||
break;
|
||||
}
|
||||
bool same_line = std::fabs(b.ylo - line_ylo) <= kBaselineEpsPx &&
|
||||
std::fabs(b.yhi - line_yhi) <= kBaselineEpsPx;
|
||||
if (!same_line || b.xlo < e.xhi - kBaselineEpsPx || b.xlo - e.xhi > kElementGapPx) {
|
||||
break;
|
||||
}
|
||||
e.xhi = b.xhi > e.xhi ? b.xhi : e.xhi;
|
||||
e.xlo = b.xlo < e.xlo ? b.xlo : e.xlo;
|
||||
e.last = n;
|
||||
}
|
||||
elements.push_back(e);
|
||||
q = e.last + 1;
|
||||
}
|
||||
|
||||
// Every element narrows about its OWN centre.
|
||||
//
|
||||
// Pivoting a label about its nearest box instead (so the label's offset
|
||||
// from the box would shrink too) was tried and REVERTED: which marker a
|
||||
// label belongs to is not encoded in the vertex data, and proximity is not
|
||||
// a stable substitute - overlapping markers sit as little as 39 px apart,
|
||||
// so under aircraft roll the nearest-box choice flips from frame to frame
|
||||
// and the label visibly jumps between two spacings. A static, slightly wide
|
||||
// label-to-box gap beats a moving one. Fixing the gap properly needs the
|
||||
// game-side marker/label association, not screen geometry.
|
||||
uint32_t narrowed_elements = 0;
|
||||
for (const Element& e : elements) {
|
||||
const float cx = 0.5f * (e.xlo + e.xhi);
|
||||
for (uint32_t n = e.first; n <= e.last; ++n) {
|
||||
const QuadBounds& b = bounds[n];
|
||||
for (uint32_t c = 0; c < 4; ++c) {
|
||||
if (guard.TestAndSet(b.vi[c])) {
|
||||
continue; // Already narrowed this swap - never compound.
|
||||
}
|
||||
uint8_t* vp = vertices + size_t(b.vi[c]) * vertex_stride + pos_offset;
|
||||
uint32_t raw;
|
||||
std::memcpy(&raw, vp, sizeof(raw));
|
||||
raw = _byteswap_ulong(raw);
|
||||
float vx;
|
||||
std::memcpy(&vx, &raw, sizeof(vx));
|
||||
float nx = cx + (vx - cx) * shrink;
|
||||
std::memcpy(&raw, &nx, sizeof(raw));
|
||||
raw = _byteswap_ulong(raw);
|
||||
std::memcpy(vp, &raw, sizeof(raw));
|
||||
}
|
||||
}
|
||||
++narrowed_elements;
|
||||
}
|
||||
static uint32_t fix_logs = 0;
|
||||
if (fix_logs < 4) {
|
||||
++fix_logs;
|
||||
REXLOG_ERROR("[AC6-WIDE] marker elements narrowed x{:g} ({} elements from {} quads, "
|
||||
"arena 0x{:08X})",
|
||||
shrink, narrowed_elements, quads, arena_base);
|
||||
}
|
||||
}
|
||||
|
||||
float WidescreenViewportShrinkX() {
|
||||
// Cheapest test first: the shrink factor is zero unless the feature is
|
||||
// enabled AND a wider-than-16:9 target is in effect, so a disabled build
|
||||
// costs one relaxed atomic load per draw and never reads the clock.
|
||||
uint32_t shrink_bits = g_ui_shrink_bits.load(std::memory_order_relaxed);
|
||||
if (!shrink_bits) {
|
||||
return 1.0f;
|
||||
}
|
||||
// World scenes ONLY: the sub-viewport misregistration matters for the
|
||||
// in-mission radar/PiP insets. Menus and the hangar use sub-viewports for
|
||||
// ordinary panels - scaling those wrecks their layout (learned the hard
|
||||
// way), and their existing constant-level treatment is already correct.
|
||||
if (!ac6::WorldRenderActiveRecently()) {
|
||||
return 1.0f;
|
||||
}
|
||||
float shrink;
|
||||
std::memcpy(&shrink, &shrink_bits, sizeof(shrink));
|
||||
return shrink;
|
||||
}
|
||||
|
||||
void WidescreenNotifySwapSource(bool gpu_composed, bool classification_valid) {
|
||||
// Mode-classified presentation: wide-scene frames (mission, opted-in
|
||||
// cinematics) fill the widened window; everything else (menus, hangar,
|
||||
// briefing, FMV, attract - whose cameras the patcher keeps at native 16:9)
|
||||
// presents letterboxed, i.e. vanilla. CPU-written frontbuffers (loading
|
||||
// images, FMV frames) letterbox even in wide scenes: those pixels are
|
||||
// 16:9-authored and must never stretch. And when no wider-than-16:9 target
|
||||
// is in effect (a 16:9 or NARROWER window), everything letterboxes - at
|
||||
// 16:9 that is pixel-identical to fill, and narrower windows get proper
|
||||
// bars instead of a vertical stretch.
|
||||
// Disabled: do no per-frame work at all. (Runs on the command processor
|
||||
// thread, once per swap.) The one thing a disabled build still owes is
|
||||
// releasing the presenter override if the cvar was switched off at
|
||||
// runtime - done once on the transition, not every frame.
|
||||
static bool s_was_enabled = false;
|
||||
if (!REXCVAR_GET(ac6_widescreen)) {
|
||||
if (s_was_enabled) {
|
||||
s_was_enabled = false;
|
||||
rex::ui::SetPresentLetterboxOverride(rex::ui::PresentLetterboxOverride::kUseCVar);
|
||||
}
|
||||
return;
|
||||
}
|
||||
s_was_enabled = true;
|
||||
// New frame: marker quads may be narrowed again (same CP thread as the
|
||||
// draws, so no synchronisation needed).
|
||||
MarkerGuardsResetForSwap();
|
||||
bool wide_scene = g_wide_scene.load(std::memory_order_relaxed);
|
||||
bool cpu_frame = classification_valid && !gpu_composed;
|
||||
// While the feature is on it owns the decision outright - fill only for a
|
||||
// wide scene rendered through the widened cameras, letterbox otherwise -
|
||||
// so the user's present_letterbox value is never consulted here.
|
||||
bool force = !wide_scene || cpu_frame || !g_target_wide.load(std::memory_order_relaxed);
|
||||
rex::ui::SetPresentLetterboxOverride(force ? rex::ui::PresentLetterboxOverride::kForceLetterbox
|
||||
: rex::ui::PresentLetterboxOverride::kForceFill);
|
||||
// Starts at 0 = "off", the presenter's actual initial state, so a disabled
|
||||
// build never logs a spurious first transition.
|
||||
static std::atomic<int> last_state{0};
|
||||
int state = force ? 1 : 0;
|
||||
if (last_state.exchange(state, std::memory_order_relaxed) != state) {
|
||||
static std::atomic<uint32_t> transition_logs{0};
|
||||
if (transition_logs.fetch_add(1, std::memory_order_relaxed) < 16) {
|
||||
REXLOG_ERROR("[AC6-WIDE] presenter letterbox {} ({})", force ? "ON" : "off",
|
||||
wide_scene ? (cpu_frame ? "wide-scene cpu-frame" : "wide-scene")
|
||||
: "front-end");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ac6
|
||||
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace rex::memory {
|
||||
class Memory;
|
||||
}
|
||||
|
||||
namespace ac6 {
|
||||
|
||||
// AC6 enhancement: arbitrary aspect ratio (ultrawide), in-mission only.
|
||||
// Called once from the D3D12 command processor's SetupContext; applies the
|
||||
// one-switch config preset (this runs after the toml is loaded, unlike app
|
||||
// create) and spawns a background thread that, while the ac6_widescreen cvar
|
||||
// is enabled, polls the game's mode-task state machine and aims the cameras
|
||||
// accordingly: inside a mission (mode task CModeTaskGame - gameplay,
|
||||
// in-engine cutscenes and pause) the stored 16:9 aspect (the static default
|
||||
// in the XEX image plus live camera objects found by signature scan) is
|
||||
// patched to ac6_widescreen_aspect so the game builds its own projection
|
||||
// wider; outside a mission everything is reverted to 16:9 so the front end
|
||||
// (menus, hangar, briefing, attract) renders vanilla. In-engine cinematics
|
||||
// outside the mission task can opt in via ac6_widescreen_cinematics. See
|
||||
// ac6_widescreen.cpp for the discovered memory layout and the mode-task
|
||||
// chain.
|
||||
void WidescreenInit(rex::memory::Memory* memory);
|
||||
|
||||
// The 2D/UI half of the ultrawide feature. Called per draw from the D3D12
|
||||
// command processor's IssueDraw with the vertex-shader float constant block
|
||||
// (256 vec4s, host-endian, mutable) and the vertex shader's guest ucode hash
|
||||
// (the game-placed target-marker shader is left full-width so markers stay
|
||||
// on target). Detects screen-space 2D transforms among the constants and
|
||||
// scales their X output row around screen center by 16:9 / target,
|
||||
// pre-squeezing 2D rendering so the presenter's fill-window stretch cancels
|
||||
// out. Applies only in wide scenes, where the fill presentation is active
|
||||
// (world rendering, and in-mission non-world draws like the pause menu).
|
||||
// Returns true if constants were modified - the caller must then invalidate
|
||||
// the vertex float constant buffer binding. No-op (atomic load + compare)
|
||||
// unless ac6_widescreen is on.
|
||||
// sub_viewport: the draw uses a sub-screen guest viewport (radar window,
|
||||
// PiP inset) - placed by the viewport, so its constants are never shrunk
|
||||
// (the viewport rect is shrunk instead; see WidescreenViewportShrinkX).
|
||||
bool WidescreenPatchUiOrtho(uint32_t* vs_float_constants, uint64_t vs_ucode_hash,
|
||||
bool sub_viewport);
|
||||
|
||||
// Called by the D3D12 texture cache whenever the frontbuffer texture is
|
||||
// requested for a swap. gpu_composed = the frontbuffer range holds GPU-written
|
||||
// pages (shared-memory tracking); classification_valid = the caller actually
|
||||
// computed it. Drives the mode-classified presentation: in-mission GPU frames
|
||||
// fill the window, everything else - the whole front end, and CPU-written
|
||||
// frames (FMV, loading images) even in-mission - presents letterboxed at
|
||||
// 16:9, i.e. vanilla.
|
||||
void WidescreenNotifySwapSource(bool gpu_composed, bool classification_valid);
|
||||
|
||||
// Whether the current draw is a target-marker draw whose quads should be
|
||||
// narrowed by WidescreenShrinkMarkerQuads (marker shader, wide scene, shrink
|
||||
// active). Cheap: an atomic load plus a hash compare.
|
||||
bool WidescreenWantsMarkerQuadFix(uint64_t vs_ucode_hash);
|
||||
|
||||
// Marker-quad geometry fix. The game bakes marker box corners as screen-space
|
||||
// positions computed through the WIDENED camera, so the positions are already
|
||||
// right for the full-width display but the box art inherits the presenter's
|
||||
// horizontal stretch (~1.34x wide at 21.5:9). Narrowing each quad's X extent
|
||||
// about its OWN centre - not screen centre - cancels that stretch for the art
|
||||
// while leaving the centre, i.e. the aim point, exactly where the game put it.
|
||||
// A per-swap bitmap makes it idempotent when the same geometry is drawn more
|
||||
// than once in a frame.
|
||||
// vertices: host pointer to the guest vertex data; stride/pos_offset in bytes
|
||||
// (position = two big-endian floats). indices: host pointer to the guest index
|
||||
// buffer, or null for sequential vertices. Quads are 4 consecutive indices.
|
||||
void WidescreenShrinkMarkerQuads(uint8_t* vertices, uint32_t vertex_stride,
|
||||
uint32_t pos_offset, const uint8_t* indices,
|
||||
bool indices_32bit, uint32_t count, uint32_t arena_base);
|
||||
|
||||
// The X shrink to apply to SUB-VIEWPORT rects (radar window, PiP inset) for
|
||||
// the current scene, or 1.0. Sub-viewport elements are placed by their
|
||||
// viewport, not their constants - the viewport rect must move with the
|
||||
// uniformly shrunk full-screen UI (scaled around the render target center)
|
||||
// while their constants stay untouched. World scenes with the in-world
|
||||
// shrink only.
|
||||
float WidescreenViewportShrinkX();
|
||||
|
||||
} // namespace ac6
|
||||
@@ -46,6 +46,30 @@ class Presenter;
|
||||
class Window;
|
||||
class Win32Window;
|
||||
|
||||
// Runtime override of the letterbox presentation decision, for a game-side
|
||||
// classifier that knows more about the frame than the present_letterbox cvar
|
||||
// does. AC6 ultrawide drives it per frame: fill while a mission renders
|
||||
// through its widened cameras, letterbox for 16:9-authored output (the whole
|
||||
// front end, and CPU-written FMV/loading frames even in-mission).
|
||||
//
|
||||
// The override REPLACES the cvar for as long as it is set, and is deliberately
|
||||
// separate from it: a feature must never write present_letterbox itself, or
|
||||
// the value leaks into the user's saved config and outlives the feature being
|
||||
// switched off. kUseCVar restores stock behaviour.
|
||||
// Thread-safe; read once per paint.
|
||||
enum class PresentLetterboxOverride : uint32_t {
|
||||
kUseCVar = 0,
|
||||
kForceLetterbox,
|
||||
kForceFill,
|
||||
};
|
||||
void SetPresentLetterboxOverride(PresentLetterboxOverride mode);
|
||||
|
||||
// Last known presentation surface size in physical pixels, stamped every
|
||||
// guest output paint. Returns false until the first paint. Thread-safe; used
|
||||
// by the AC6 ultrawide feature to derive the target aspect ratio from the
|
||||
// actual window instead of a manually configured value.
|
||||
bool GetPresentSurfaceSize(uint32_t* width, uint32_t* height);
|
||||
|
||||
class UIDrawContext {
|
||||
public:
|
||||
UIDrawContext(const UIDrawContext& context) = delete;
|
||||
|
||||
@@ -96,6 +96,12 @@ class SharedMemory {
|
||||
// regions in those pages.
|
||||
void RangeWrittenByGpu(uint32_t start, uint32_t length);
|
||||
|
||||
// Whether any page in the range currently holds GPU-generated data (set by
|
||||
// RangeWrittenByGpu, cleared when the CPU overwrites the page). Used by the
|
||||
// AC6 ultrawide swap classification: a presented frontbuffer with no
|
||||
// GPU-written pages was composed by the CPU (FMV frames, loading images).
|
||||
bool IsAnyPageGpuWritten(uint32_t start, uint32_t length);
|
||||
|
||||
protected:
|
||||
SharedMemory(memory::Memory& memory);
|
||||
// Call in implementation-specific initialization.
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include <rex/ui/d3d12/d3d12_util.h>
|
||||
|
||||
#include "../../../../../src/ac6_backend_fixes/ac6_backend_hooks.h"
|
||||
#include "../../../../../src/ac6_backend_fixes/ac6_widescreen.h"
|
||||
#include "../../../../../src/ac6_native_graphics.h"
|
||||
#include "../../../../../src/render_hooks.h"
|
||||
|
||||
@@ -1119,6 +1120,10 @@ bool D3D12CommandProcessor::SetupContext() {
|
||||
VertexBufferMemoryInvalidationCallbackThunk, this);
|
||||
}
|
||||
|
||||
// AC6: start the ultrawide camera-aspect patcher (idles unless the
|
||||
// ac6_widescreen cvar is enabled).
|
||||
ac6::WidescreenInit(memory_);
|
||||
|
||||
// Initialize the render target cache before configuring binding - need to
|
||||
// know if using rasterizer-ordered views for the bindless root signature.
|
||||
render_target_cache_ = std::make_unique<D3D12RenderTargetCache>(
|
||||
@@ -2674,6 +2679,59 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, uint3
|
||||
ac6::NotifyWorldCompositorDraw();
|
||||
}
|
||||
|
||||
// AC6 ultrawide: pre-squeeze the game's screen-space 2D transforms in the
|
||||
// VS float constants so the presenter's fill-window stretch cancels out
|
||||
// where the fill presentation is active (in-mission; the gameplay HUD's
|
||||
// excluded marker shader is left full-width so world-projected target
|
||||
// markers stay aligned). No-op unless ac6_widescreen + ac6_widescreen_ui
|
||||
// are enabled.
|
||||
// Sub-screen guest viewport (radar window, PiP inset): |x scale| is half
|
||||
// the viewport width in guest pixels - well under the full 640.
|
||||
float ac6_vp_xscale;
|
||||
std::memcpy(&ac6_vp_xscale, ®s.values[XE_GPU_REG_PA_CL_VPORT_XSCALE],
|
||||
sizeof(ac6_vp_xscale));
|
||||
float ac6_vp_xscale_abs = ac6_vp_xscale < 0.0f ? -ac6_vp_xscale : ac6_vp_xscale;
|
||||
bool ac6_sub_viewport = ac6_vp_xscale_abs >= 1.0f && ac6_vp_xscale_abs < 576.0f;
|
||||
if (ac6::WidescreenPatchUiOrtho(®ister_file_->values[XE_GPU_REG_SHADER_CONSTANT_000_X],
|
||||
vertex_shader ? vertex_shader->ucode_data_hash() : 0,
|
||||
ac6_sub_viewport)) {
|
||||
cbuffer_binding_float_vertex_.up_to_date = false;
|
||||
}
|
||||
// AC6 ultrawide: narrow target-marker quads about their own centres so the
|
||||
// fill-window stretch renders them square while the game-computed aim points
|
||||
// stay put (see WidescreenShrinkMarkerQuads). The game draws them as a quad
|
||||
// list of screen-space positions in a CPU-written arena, so the edit is made
|
||||
// in guest memory here - before the vertex buffers are requested below - and
|
||||
// is naturally transient (the arena is rewritten every frame).
|
||||
if (vertex_shader && primitive_type == xenos::PrimitiveType::kQuadList &&
|
||||
ac6::WidescreenWantsMarkerQuadFix(vertex_shader->ucode_data_hash())) {
|
||||
for (const Shader::VertexBinding& vb : vertex_shader->vertex_bindings()) {
|
||||
if (vb.attributes.empty() || !vb.stride_words) {
|
||||
continue;
|
||||
}
|
||||
xenos::xe_gpu_vertex_fetch_t vf = regs.GetVertexFetch(vb.fetch_constant);
|
||||
if (vf.type != xenos::FetchConstantType::kVertex) {
|
||||
continue;
|
||||
}
|
||||
uint32_t arena_base = vf.address << 2;
|
||||
uint8_t* vertex_data = memory_->TranslatePhysical(arena_base);
|
||||
if (!vertex_data) {
|
||||
continue;
|
||||
}
|
||||
const uint8_t* index_data = nullptr;
|
||||
bool indices_32bit = false;
|
||||
if (index_buffer_info && index_buffer_info->guest_base) {
|
||||
index_data = memory_->TranslatePhysical(index_buffer_info->guest_base);
|
||||
indices_32bit = index_buffer_info->format == xenos::IndexFormat::kInt32;
|
||||
}
|
||||
// The position attribute is the one at offset 0 of the vertex.
|
||||
ac6::WidescreenShrinkMarkerQuads(vertex_data, vb.stride_words * sizeof(uint32_t),
|
||||
uint32_t(vb.attributes[0].fetch_instr.attributes.offset) *
|
||||
sizeof(uint32_t),
|
||||
index_data, indices_32bit, index_count, arena_base);
|
||||
}
|
||||
}
|
||||
|
||||
if (!BeginSubmission(true)) {
|
||||
return false;
|
||||
}
|
||||
@@ -4072,6 +4130,30 @@ void D3D12CommandProcessor::UpdateFixedFunctionState(
|
||||
viewport.Height = float(viewport_info.xy_extent[1]);
|
||||
viewport.MinDepth = viewport_info.z_min;
|
||||
viewport.MaxDepth = viewport_info.z_max;
|
||||
|
||||
// AC6 ultrawide: sub-viewport draws (radar window, PiP inset) are placed
|
||||
// by their VIEWPORT rect, not their shader constants - shrinking their
|
||||
// constants moves content around the viewport center instead of the screen
|
||||
// center (the radar-misregistration bug). Shrink the viewport rect (and
|
||||
// its scissor) around the render target center instead; the constant-level
|
||||
// patch skips these draws.
|
||||
bool ac6_vp_scaled = false;
|
||||
float ac6_vp_shrink = ac6::WidescreenViewportShrinkX();
|
||||
if (ac6_vp_shrink != 1.0f && !normalized_depth_control.z_enable &&
|
||||
(viewport.TopLeftX >= 8.0f || viewport.TopLeftY >= 8.0f)) {
|
||||
// Placed UI insets only (radar window, PiP): inset-sized, depth
|
||||
// disabled, and offset from the origin. Internal render-to-texture
|
||||
// passes (half-res effects, shadows, EDRAM ops) are depth-enabled
|
||||
// and/or origin-anchored - scaling THEIR viewports white-outs the world
|
||||
// (learned the hard way).
|
||||
float ac6_full_width = 1280.0f * float(texture_cache_->draw_resolution_scale_x());
|
||||
if (viewport.Width >= 1.0f && viewport.Width < ac6_full_width * 0.35f) {
|
||||
float ac6_center_x = ac6_full_width * 0.5f;
|
||||
viewport.TopLeftX = ac6_center_x + (viewport.TopLeftX - ac6_center_x) * ac6_vp_shrink;
|
||||
viewport.Width *= ac6_vp_shrink;
|
||||
ac6_vp_scaled = true;
|
||||
}
|
||||
}
|
||||
SetViewport(viewport);
|
||||
|
||||
// Scissor.
|
||||
@@ -4080,6 +4162,13 @@ void D3D12CommandProcessor::UpdateFixedFunctionState(
|
||||
scissor_rect.top = LONG(scissor.offset[1]);
|
||||
scissor_rect.right = LONG(scissor.offset[0] + scissor.extent[0]);
|
||||
scissor_rect.bottom = LONG(scissor.offset[1] + scissor.extent[1]);
|
||||
if (ac6_vp_scaled) {
|
||||
float ac6_center_x = 1280.0f * float(texture_cache_->draw_resolution_scale_x()) * 0.5f;
|
||||
scissor_rect.left = LONG(ac6_center_x + (float(scissor_rect.left) - ac6_center_x) *
|
||||
ac6_vp_shrink);
|
||||
scissor_rect.right = LONG(ac6_center_x + (float(scissor_rect.right) - ac6_center_x) *
|
||||
ac6_vp_shrink + 0.5f);
|
||||
}
|
||||
SetScissorRect(scissor_rect);
|
||||
|
||||
if (render_target_cache_->GetPath() == RenderTargetCache::Path::kHostRenderTargets) {
|
||||
|
||||
@@ -38,8 +38,11 @@
|
||||
#include <rex/hash.h>
|
||||
|
||||
#include "../../../../../src/ac6_backend_fixes/ac6_backend_hooks.h"
|
||||
#include "../../../../../src/ac6_backend_fixes/ac6_widescreen.h"
|
||||
#include "../../../../../src/ac6_texture_overrides.h"
|
||||
|
||||
REXCVAR_DECLARE(bool, ac6_widescreen);
|
||||
|
||||
namespace rex::graphics::d3d12 {
|
||||
|
||||
REXCVAR_DEFINE_BOOL(d3d12_log_bc1_diagnostics, false, "GPU/D3D12",
|
||||
@@ -1600,6 +1603,37 @@ ID3D12Resource* D3D12TextureCache::RequestSwapTexture(D3D12_SHADER_RESOURCE_VIEW
|
||||
// Only texture->key, not the result of BindingInfoFromFetchConstant, contains
|
||||
// whether the texture is scaled.
|
||||
key = texture->key();
|
||||
// AC6 ultrawide: classify this swap for the presenter via the shared
|
||||
// memory's GPU-written page tracking (authoritative: RangeWrittenByGpu sets
|
||||
// it on resolves, a CPU overwrite clears it - unlike the scaled-resolve
|
||||
// marking, which can serve upsampled CPU data while still reading as
|
||||
// "scaled"). A presented frontbuffer with no GPU-written pages was composed
|
||||
// by the CPU (FMV frames, loading images) - 16:9-authored pixels that must
|
||||
// be letterboxed rather than stretched to the window.
|
||||
{
|
||||
bool gpu_composed = false;
|
||||
// The page query and its log only matter (and only cost) with the
|
||||
// feature on; the notify itself early-outs when disabled.
|
||||
if (REXCVAR_GET(ac6_widescreen)) {
|
||||
texture_util::TextureGuestLayout swap_layout = key.GetGuestLayout();
|
||||
uint32_t swap_extent = swap_layout.base.level_data_extent_bytes;
|
||||
gpu_composed =
|
||||
swap_extent && shared_memory().IsAnyPageGpuWritten(key.base_page << 12, swap_extent);
|
||||
// Log frontbuffer address + composition on state change (capped).
|
||||
static uint32_t ac6_last_swap_state = UINT32_MAX;
|
||||
static uint32_t ac6_swap_state_logs = 0;
|
||||
uint32_t ac6_swap_state = (uint32_t(key.base_page) << 1) | (gpu_composed ? 1u : 0u);
|
||||
if (ac6_swap_state != ac6_last_swap_state) {
|
||||
ac6_last_swap_state = ac6_swap_state;
|
||||
if (ac6_swap_state_logs < 32) {
|
||||
++ac6_swap_state_logs;
|
||||
REXGPU_ERROR("[AC6-SWAP] frontbuffer base_page={:05X} gpu_written={} extent=0x{:X}",
|
||||
uint32_t(key.base_page), gpu_composed ? 1 : 0, swap_extent);
|
||||
}
|
||||
}
|
||||
}
|
||||
ac6::WidescreenNotifySwapSource(gpu_composed, true);
|
||||
}
|
||||
if (width_unscaled_out) {
|
||||
*width_unscaled_out = key.GetWidth();
|
||||
}
|
||||
|
||||
@@ -380,6 +380,31 @@ void SharedMemory::MakeRangeValid(uint32_t start, uint32_t length, bool written_
|
||||
}
|
||||
}
|
||||
|
||||
bool SharedMemory::IsAnyPageGpuWritten(uint32_t start, uint32_t length) {
|
||||
if (!length || start >= kBufferSize) {
|
||||
return false;
|
||||
}
|
||||
length = std::min(length, kBufferSize - start);
|
||||
uint32_t page_first = start >> page_size_log2_;
|
||||
uint32_t page_last = (start + length - 1) >> page_size_log2_;
|
||||
uint32_t block_first = page_first >> 6;
|
||||
uint32_t block_last = page_last >> 6;
|
||||
auto global_lock = global_critical_region_.Acquire();
|
||||
for (uint32_t i = block_first; i <= block_last; ++i) {
|
||||
uint64_t bits = UINT64_MAX;
|
||||
if (i == block_first) {
|
||||
bits &= ~((uint64_t(1) << (page_first & 63)) - 1);
|
||||
}
|
||||
if (i == block_last && (page_last & 63) != 63) {
|
||||
bits &= (uint64_t(1) << ((page_last & 63) + 1)) - 1;
|
||||
}
|
||||
if (system_page_flags_valid_and_gpu_written_[i] & bits) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void SharedMemory::UnlinkWatchRange(WatchRange* range) {
|
||||
uint32_t bucket = range->page_first << page_size_log2_ >> kWatchBucketSizeLog2;
|
||||
WatchNode* node = range->node_first;
|
||||
|
||||
+42
-2
@@ -284,6 +284,41 @@ GuestOutputPaintConfig BuildGuestOutputPaintConfigFromCVar() {
|
||||
namespace rex {
|
||||
namespace ui {
|
||||
|
||||
// See SetPresentLetterboxOverride in the header. Stamped from the GPU
|
||||
// subsystem (swap-source classification), read by GetGuestOutputPaintFlow.
|
||||
static std::atomic<PresentLetterboxOverride> g_present_letterbox_override{
|
||||
PresentLetterboxOverride::kUseCVar};
|
||||
|
||||
void SetPresentLetterboxOverride(PresentLetterboxOverride mode) {
|
||||
g_present_letterbox_override.store(mode, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// Whether the letterbox path is enabled for this paint: the override when one
|
||||
// is set, the cvar otherwise.
|
||||
static bool PresentLetterboxEnabled() {
|
||||
switch (g_present_letterbox_override.load(std::memory_order_relaxed)) {
|
||||
case PresentLetterboxOverride::kForceLetterbox:
|
||||
return true;
|
||||
case PresentLetterboxOverride::kForceFill:
|
||||
return false;
|
||||
default:
|
||||
return REXCVAR_GET(present_letterbox);
|
||||
}
|
||||
}
|
||||
|
||||
// See GetPresentSurfaceSize in the header. Width in the high 32 bits.
|
||||
static std::atomic<uint64_t> g_present_surface_size{0};
|
||||
|
||||
bool GetPresentSurfaceSize(uint32_t* width, uint32_t* height) {
|
||||
uint64_t packed = g_present_surface_size.load(std::memory_order_relaxed);
|
||||
if (!packed) {
|
||||
return false;
|
||||
}
|
||||
*width = uint32_t(packed >> 32);
|
||||
*height = uint32_t(packed);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Presenter::FatalErrorHostGpuLossCallback([[maybe_unused]] bool is_responsible,
|
||||
[[maybe_unused]] bool statically_from_ui_thread) {
|
||||
rex::FatalError("Graphics device lost (probably due to an internal error)");
|
||||
@@ -880,6 +915,11 @@ Presenter::GuestOutputPaintFlow Presenter::GetGuestOutputPaintFlow(
|
||||
return flow;
|
||||
}
|
||||
|
||||
// Publish the surface size for aspect-ratio auto-detection (AC6 ultrawide).
|
||||
g_present_surface_size.store((uint64_t(surface_width_in_paint_connection_) << 32) |
|
||||
surface_height_in_paint_connection_,
|
||||
std::memory_order_relaxed);
|
||||
|
||||
flow.properties = properties;
|
||||
|
||||
// Multiplication-division rounding to the nearest.
|
||||
@@ -929,7 +969,7 @@ Presenter::GuestOutputPaintFlow Presenter::GetGuestOutputPaintFlow(
|
||||
output_height = rescale_unsigned(surface_height_in_paint_connection_, 100, present_safe_area);
|
||||
letterbox = true;
|
||||
}
|
||||
if (letterbox && REXCVAR_GET(present_letterbox)) {
|
||||
if (letterbox && PresentLetterboxEnabled()) {
|
||||
output_width = rescale_unsigned(surface_height_in_paint_connection_ * 100,
|
||||
properties.display_aspect_ratio_x,
|
||||
properties.display_aspect_ratio_y * present_safe_area);
|
||||
@@ -964,7 +1004,7 @@ Presenter::GuestOutputPaintFlow Presenter::GetGuestOutputPaintFlow(
|
||||
output_width = rescale_unsigned(surface_width_in_paint_connection_, 100, present_safe_area);
|
||||
letterbox = true;
|
||||
}
|
||||
if (letterbox && REXCVAR_GET(present_letterbox)) {
|
||||
if (letterbox && PresentLetterboxEnabled()) {
|
||||
output_height = rescale_unsigned(surface_width_in_paint_connection_ * 100,
|
||||
properties.display_aspect_ratio_y,
|
||||
properties.display_aspect_ratio_x * present_safe_area);
|
||||
|
||||
Reference in New Issue
Block a user