skate3recomp-multiplayer/src/skate3_ultrawide_guest.h

188 lines
5.8 KiB
C++

#pragma once
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <rex/cvar.h>
#include <rex/graphics/native_guest_renderer.h>
// Guest-side ultrawide support: the native renderer draws true widescreen
// frames (wide guest output + Hor+ projection), but the game's own CPU
// culling still tests against its 16:9 view frustum and would drop side
// content before it ever reaches the renderer. The frustum patch scope below
// is injected into the recompiled cull routine (see
// cmake/ApplySkate3CodegenPatches.cmake) and widens the cull planes in guest
// memory to match the wide view.
inline bool Skate3UltrawideHasExpandedTargetAspect() {
if (!rex::cvar::Query<bool>("skate3_ultrawide") ||
!rex::graphics::IsNativeGuestOutputActive()) {
return false;
}
return rex::cvar::Query<double>("skate3_ultrawide_target_aspect") >
(16.0 / 9.0) + 0.01;
}
inline bool Skate3UltrawideShouldWidenGameCullFrustum(u8* base, uint32_t cull_block) {
// Every cull block reaching the vectorized cull job gets its left/right
// planes widened: DoFrustumCulling reads them unconditionally, including
// blocks with no extra cull volumes (the word at +5260); gating on that
// count left 16:9 culling holes at the frame edges in the areas whose
// blocks carry none. Over-inclusion is safe (a few extra submitted
// meshes); the plane sanity checks below still veto implausible data.
(void)base;
return cull_block != 0 && Skate3UltrawideHasExpandedTargetAspect() &&
rex::cvar::Query<bool>("skate3_ultrawide_widen_game_frustum");
}
struct Skate3UltrawideGameFrustumPatchScope {
Skate3UltrawideGameFrustumPatchScope(PPCContext& ctx_, u8* base_, uint32_t cull_block)
: ctx(ctx_), base(base_) {
if (!Skate3UltrawideShouldWidenGameCullFrustum(base, cull_block)) {
return;
}
const double target_aspect = std::clamp(
rex::cvar::Query<double>("skate3_ultrawide_target_aspect"), 16.0 / 9.0, 8.0);
side_scale = float((16.0 / 9.0) / target_aspect);
if (!(side_scale > 0.0f && side_scale < 0.999f)) {
return;
}
block = cull_block;
PlaneBits current_left_bits = LoadPlaneBits(kLeftPlaneOffset);
PlaneBits current_right_bits = LoadPlaneBits(kRightPlaneOffset);
WidenedPlaneCache& cache = CacheForBlock();
if (cache.valid && cache.side_scale == side_scale &&
current_left_bits == cache.widened[0] && current_right_bits == cache.widened[1]) {
return;
}
cache.original[0] = current_left_bits;
cache.original[1] = current_right_bits;
Plane left = LoadPlane(kLeftPlaneOffset);
Plane right = LoadPlane(kRightPlaneOffset);
Plane widened_left{};
Plane widened_right{};
for (size_t i = 0; i < left.size(); ++i) {
const float center = (left[i] + right[i]) * 0.5f;
const float side = (left[i] - right[i]) * 0.5f * side_scale;
widened_left[i] = center + side;
widened_right[i] = center - side;
}
if (!NormalizePlane(widened_left) || !NormalizePlane(widened_right)) {
return;
}
StorePlane(kLeftPlaneOffset, widened_left);
StorePlane(kRightPlaneOffset, widened_right);
cache.widened[0] = LoadPlaneBits(kLeftPlaneOffset);
cache.widened[1] = LoadPlaneBits(kRightPlaneOffset);
cache.side_scale = side_scale;
cache.valid = true;
active = true;
}
~Skate3UltrawideGameFrustumPatchScope() = default;
Skate3UltrawideGameFrustumPatchScope(const Skate3UltrawideGameFrustumPatchScope&) = delete;
Skate3UltrawideGameFrustumPatchScope& operator=(const Skate3UltrawideGameFrustumPatchScope&) =
delete;
private:
using Plane = std::array<float, 4>;
using PlaneBits = std::array<uint32_t, 4>;
struct WidenedPlaneCache {
uint32_t block = 0;
std::array<PlaneBits, 2> original{};
std::array<PlaneBits, 2> widened{};
float side_scale = 1.0f;
bool valid = false;
};
static constexpr uint32_t kLeftPlaneOffset = 5152;
static constexpr uint32_t kRightPlaneOffset = 5168;
float LoadFloat(uint32_t address) const {
PPCRegister value{};
value.u32 = REX_LOAD_U32(address);
return value.f32;
}
void StoreFloat(uint32_t address, float number) const {
PPCRegister value{};
value.f32 = number;
REX_STORE_U32(address, value.u32);
}
Plane LoadPlane(uint32_t offset) const {
Plane plane{};
for (uint32_t i = 0; i < plane.size(); ++i) {
plane[i] = LoadFloat(block + offset + i * 4);
}
return plane;
}
void StorePlane(uint32_t offset, const Plane& plane) const {
for (uint32_t i = 0; i < plane.size(); ++i) {
StoreFloat(block + offset + i * 4, plane[i]);
}
}
PlaneBits LoadPlaneBits(uint32_t offset) const {
PlaneBits plane{};
for (uint32_t i = 0; i < plane.size(); ++i) {
plane[i] = REX_LOAD_U32(block + offset + i * 4);
}
return plane;
}
WidenedPlaneCache& CacheForBlock() {
static thread_local std::array<WidenedPlaneCache, 16> cache_entries{};
WidenedPlaneCache* empty = nullptr;
for (WidenedPlaneCache& entry : cache_entries) {
if (entry.valid && entry.block == block) {
return entry;
}
if (!entry.valid && !empty) {
empty = &entry;
}
}
WidenedPlaneCache& entry = empty ? *empty : cache_entries[block % cache_entries.size()];
entry = {};
entry.block = block;
return entry;
}
static bool NormalizePlane(Plane& plane) {
const float length =
std::sqrt(plane[0] * plane[0] + plane[1] * plane[1] + plane[2] * plane[2]);
if (!(length > 0.00001f) || !std::isfinite(length)) {
return false;
}
const float inv_length = 1.0f / length;
for (float& value : plane) {
value *= inv_length;
if (!std::isfinite(value)) {
return false;
}
}
return true;
}
uint32_t block = 0;
PPCContext& ctx;
u8* base = nullptr;
float side_scale = 1.0f;
bool active = false;
};