skate3recomp-multiplayer/src/skate3_ultrawide_guest.h
2026-06-10 05:56:34 +01:00

614 lines
21 KiB
C++

#pragma once
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <cstdlib>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <unordered_map>
#include <string>
#include <string_view>
#include <mutex>
#include <rex/cvar.h>
#include <rex/graphics/ultrawide_debug.h>
#include <rex/logging.h>
inline bool Skate3UltrawideHasExpandedTargetAspect() {
if (!rex::cvar::Query<bool>("skate3_ultrawide") ||
!rex::cvar::Query<bool>("skate3_ultrawide_hor_plus") ||
!rex::graphics::ultrawide_debug::IsSkate3GameplayUltrawideActive()) {
return false;
}
return rex::cvar::Query<double>("skate3_ultrawide_target_aspect") >
(16.0 / 9.0) + 0.01;
}
inline float Skate3UltrawideFloatFromU32(uint32_t value) {
union {
uint32_t u;
float f;
} bits{value};
return bits.f;
}
inline uint32_t Skate3UltrawideU32FromFloat(float value) {
union {
float f;
uint32_t u;
} bits{value};
return bits.u;
}
inline double Skate3UltrawideMaybeOverrideGameplayFrustumAspect(uint32_t site_id,
double native_aspect) {
if (!Skate3UltrawideHasExpandedTargetAspect() ||
!rex::cvar::Query<bool>("skate3_ultrawide_force_main_visibility_flags") ||
!std::isfinite(native_aspect) || native_aspect <= 0.0) {
return native_aspect;
}
const double forced_aspect = std::clamp(
rex::cvar::Query<double>("skate3_ultrawide_target_aspect"), 16.0 / 9.0, 8.0);
if (forced_aspect <= native_aspect + 0.001) {
return native_aspect;
}
const uint32_t native_milli = uint32_t(std::clamp(native_aspect * 1000.0, 0.0, 100000.0));
const uint32_t forced_milli = uint32_t(std::clamp(forced_aspect * 1000.0, 0.0, 100000.0));
rex::graphics::ultrawide_debug::RecordScreenCallback(
rex::graphics::ultrawide_debug::ScreenCallbackKind::kWidth, site_id, native_milli);
rex::graphics::ultrawide_debug::RecordScreenCallbackResult(
rex::graphics::ultrawide_debug::ScreenCallbackKind::kWidth, site_id, native_milli,
forced_milli);
return forced_aspect;
}
inline void Skate3UltrawideMaybeExpandMainCullRange(PPCContext& ctx, u8* base,
uint32_t min_addr,
uint32_t max_addr) {
if (!Skate3UltrawideHasExpandedTargetAspect()) {
return;
}
const double target_aspect = std::clamp(
rex::cvar::Query<double>("skate3_ultrawide_target_aspect"), 16.0 / 9.0, 8.0);
const float scale = float(target_aspect / (16.0 / 9.0));
if (scale <= 1.0f) {
return;
}
const float native_min = Skate3UltrawideFloatFromU32(REX_LOAD_U32(min_addr));
const float native_max = Skate3UltrawideFloatFromU32(REX_LOAD_U32(max_addr));
if (!std::isfinite(native_min) || !std::isfinite(native_max) ||
native_max <= native_min) {
return;
}
const float center = (native_min + native_max) * 0.5f;
const float half_width = (native_max - native_min) * 0.5f * scale;
const float widened_min = center - half_width;
const float widened_max = center + half_width;
if (!std::isfinite(widened_min) || !std::isfinite(widened_max)) {
return;
}
REX_STORE_U32(min_addr, Skate3UltrawideU32FromFloat(widened_min));
REX_STORE_U32(max_addr, Skate3UltrawideU32FromFloat(widened_max));
}
inline uint32_t Skate3UltrawideRecordScreenHeight(uint32_t site_id, uint32_t native_height) {
rex::graphics::ultrawide_debug::RecordScreenCallback(
rex::graphics::ultrawide_debug::ScreenCallbackKind::kHeight, site_id, native_height);
return native_height;
}
inline uint32_t Skate3UltrawideMaybeOverrideScreenFlag(
rex::graphics::ultrawide_debug::ScreenCallbackKind kind, uint32_t site_id,
uint32_t native_value) {
const bool selected =
rex::graphics::ultrawide_debug::RecordScreenCallback(kind, site_id, native_value);
return selected ? 1 : native_value;
}
inline uint32_t Skate3UltrawideMaybeForceMainVisibilityFlag(uint32_t site_id,
uint32_t native_value) {
rex::graphics::ultrawide_debug::RecordScreenCallback(
rex::graphics::ultrawide_debug::ScreenCallbackKind::kScreenFlag, site_id, native_value);
if (!Skate3UltrawideHasExpandedTargetAspect() ||
!rex::cvar::Query<bool>("skate3_ultrawide_force_main_visibility_flags")) {
return native_value;
}
rex::graphics::ultrawide_debug::RecordScreenCallbackResult(
rex::graphics::ultrawide_debug::ScreenCallbackKind::kScreenFlag, site_id, native_value, 1);
return 1;
}
inline uint32_t Skate3UltrawideMaybeForceHighestLodState(uint32_t site_id, uint32_t object,
uint32_t native_state) {
if (!Skate3UltrawideHasExpandedTargetAspect() ||
!rex::cvar::Query<bool>("skate3_ultrawide_force_main_visibility_flags") ||
native_state == 0) {
return native_state;
}
return 0;
}
inline thread_local bool g_skate3_ultrawide_force_main_cull_classifier = false;
struct Skate3UltrawideMainCullClassifierScope {
explicit Skate3UltrawideMainCullClassifierScope(bool enabled)
: previous(g_skate3_ultrawide_force_main_cull_classifier) {
g_skate3_ultrawide_force_main_cull_classifier =
previous || (enabled && Skate3UltrawideHasExpandedTargetAspect());
}
~Skate3UltrawideMainCullClassifierScope() {
g_skate3_ultrawide_force_main_cull_classifier = previous;
}
bool previous;
};
inline uint32_t Skate3UltrawideMaybeForceMainCullClassifierHalfword(uint32_t native_value) {
if (!g_skate3_ultrawide_force_main_cull_classifier ||
!rex::cvar::Query<bool>("skate3_ultrawide_force_main_visibility_flags")) {
return native_value;
}
return 1;
}
inline bool Skate3UltrawideShouldForceWorldMainCullDescriptor(u8* base, uint32_t descriptor) {
if (!Skate3UltrawideHasExpandedTargetAspect() ||
!rex::cvar::Query<bool>("skate3_ultrawide_force_main_visibility_flags") ||
descriptor < 512) {
return false;
}
constexpr uint32_t kWorldVtable = 0x8230B278;
const uint32_t object = descriptor - 512;
if (REX_LOAD_U32(object) != kWorldVtable) {
return false;
}
const uint32_t output_flags = REX_LOAD_U32(descriptor + 12);
return output_flags != 0 && output_flags == REX_LOAD_U32(object + 496);
}
inline bool Skate3UltrawideShouldWidenGameCullFrustum(u8* base, uint32_t cull_block) {
if (!Skate3UltrawideHasExpandedTargetAspect() ||
!rex::cvar::Query<bool>("skate3_ultrawide_widen_game_frustum") || !cull_block) {
return false;
}
return REX_LOAD_U32(cull_block + 5260) != 0;
}
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;
};
inline std::filesystem::path Skate3UltrawideCacheLogPath(const char* filename);
enum Skate3UltrawideObjectStageBits : uint32_t {
kSkate3ObjectStageActive = 1u << 0,
kSkate3ObjectStagePrep = 1u << 1,
kSkate3ObjectStageList = 1u << 2,
kSkate3ObjectStageMain = 1u << 3,
};
struct Skate3UltrawideObjectStageRecord {
uint32_t bits = 0;
uint32_t vtable = 0;
uint32_t target = 0;
uint32_t first_site = 0;
uint32_t last_site = 0;
uint32_t aux0 = 0;
uint32_t aux1 = 0;
};
inline uint64_t& Skate3UltrawideObjectStageFrameId() {
static uint64_t frame = 0;
return frame;
}
inline uint32_t& Skate3UltrawideObjectStageRenderer() {
static uint32_t renderer = 0;
return renderer;
}
inline std::unordered_map<uint32_t, Skate3UltrawideObjectStageRecord>&
Skate3UltrawideObjectStageRecords() {
static std::unordered_map<uint32_t, Skate3UltrawideObjectStageRecord> records;
return records;
}
inline std::mutex& Skate3UltrawideObjectStageMutex() {
static std::mutex mutex;
return mutex;
}
inline std::ofstream& Skate3UltrawideObjectStageLogFile() {
static std::ofstream log_file;
if (!log_file.is_open()) {
std::filesystem::path log_path =
Skate3UltrawideCacheLogPath("skate3_ultrawide_object_stage_trace.log");
std::error_code ec;
if (log_path.has_parent_path()) {
std::filesystem::create_directories(log_path.parent_path(), ec);
}
log_file.open(log_path, std::ios::out | std::ios::trunc);
if (log_file.is_open()) {
log_file << "seq,frame,kind,marker,renderer,object,bits,site,vtable,target,aux0,aux1,"
"active,prep,list,main,missing_main,total\n";
}
}
return log_file;
}
inline bool Skate3UltrawideObjectStageTraceEnabled() {
return rex::cvar::Query<bool>("skate3_ultrawide_object_stage_trace_continuous") ||
rex::cvar::Query<int32_t>("skate3_ultrawide_object_stage_trace_frames_remaining") > 0 ||
rex::cvar::Query<int32_t>("skate3_ultrawide_object_stage_marker") != 0;
}
inline void Skate3UltrawideWriteObjectStageLog(
uint64_t frame, const char* kind, uint32_t marker, uint32_t renderer, uint32_t object,
uint32_t bits, uint32_t site, uint32_t vtable, uint32_t target, uint32_t aux0,
uint32_t aux1, uint32_t active_count, uint32_t prep_count, uint32_t list_count,
uint32_t main_count, uint32_t missing_main_count, uint32_t total_count) {
if (!rex::cvar::Query<bool>("skate3_ultrawide_object_stage_trace_to_disk")) {
return;
}
static std::atomic<uint64_t> sequence{0};
std::ofstream& log_file = Skate3UltrawideObjectStageLogFile();
if (!log_file.is_open()) {
return;
}
log_file << std::dec << sequence.fetch_add(1, std::memory_order_relaxed) << ',' << frame
<< ',' << kind << ',' << marker << ",0x" << std::hex << renderer << ",0x"
<< object << ",0x" << bits << ",0x" << site << ",0x" << vtable << ",0x"
<< target << ",0x" << aux0 << ",0x" << aux1 << ',' << std::dec << active_count
<< ',' << prep_count << ',' << list_count << ',' << main_count << ','
<< missing_main_count << ',' << total_count << '\n';
log_file.flush();
}
inline void Skate3UltrawideFinalizeObjectStageFrameLocked(const char* reason) {
auto& records = Skate3UltrawideObjectStageRecords();
if (records.empty()) {
return;
}
const uint32_t marker =
static_cast<uint32_t>(rex::cvar::Query<int32_t>("skate3_ultrawide_object_stage_marker"));
const bool enabled = Skate3UltrawideObjectStageTraceEnabled();
uint32_t remaining =
static_cast<uint32_t>(std::max(0, rex::cvar::Query<int32_t>(
"skate3_ultrawide_object_stage_trace_frames_remaining")));
if (!enabled) {
records.clear();
return;
}
uint32_t active_count = 0;
uint32_t prep_count = 0;
uint32_t list_count = 0;
uint32_t main_count = 0;
uint32_t missing_main_count = 0;
for (const auto& [object, record] : records) {
active_count += (record.bits & kSkate3ObjectStageActive) ? 1 : 0;
prep_count += (record.bits & kSkate3ObjectStagePrep) ? 1 : 0;
list_count += (record.bits & kSkate3ObjectStageList) ? 1 : 0;
main_count += (record.bits & kSkate3ObjectStageMain) ? 1 : 0;
if ((record.bits & (kSkate3ObjectStagePrep | kSkate3ObjectStageList)) &&
!(record.bits & kSkate3ObjectStageMain)) {
++missing_main_count;
}
}
const uint64_t frame = Skate3UltrawideObjectStageFrameId();
const uint32_t renderer = Skate3UltrawideObjectStageRenderer();
Skate3UltrawideWriteObjectStageLog(frame, reason, marker, renderer, 0, 0, 0, 0, 0, 0, 0,
active_count, prep_count, list_count, main_count,
missing_main_count, static_cast<uint32_t>(records.size()));
uint32_t emitted_missing = 0;
const uint32_t missing_limit = static_cast<uint32_t>(std::clamp(
rex::cvar::Query<int32_t>("skate3_ultrawide_object_stage_missing_limit"), 0, 4096));
for (const auto& [object, record] : records) {
if (!((record.bits & (kSkate3ObjectStagePrep | kSkate3ObjectStageList)) &&
!(record.bits & kSkate3ObjectStageMain))) {
continue;
}
if (emitted_missing++ >= missing_limit) {
break;
}
Skate3UltrawideWriteObjectStageLog(frame, "missing-main", marker, renderer, object,
record.bits, record.last_site, record.vtable,
record.target, record.aux0, record.aux1, active_count,
prep_count, list_count, main_count, missing_main_count,
static_cast<uint32_t>(records.size()));
}
if (remaining > 0) {
rex::cvar::SetFlagByName("skate3_ultrawide_object_stage_trace_frames_remaining",
std::to_string(remaining - 1));
}
if (marker != 0) {
rex::cvar::SetFlagByName("skate3_ultrawide_object_stage_marker", "0");
}
records.clear();
}
inline void Skate3UltrawideBeginObjectStageFrame(uint32_t site_id, uint32_t renderer) {
if (!Skate3UltrawideHasExpandedTargetAspect()) {
return;
}
if (renderer < 0x10000) {
return;
}
std::lock_guard lock(Skate3UltrawideObjectStageMutex());
Skate3UltrawideFinalizeObjectStageFrameLocked("frame");
++Skate3UltrawideObjectStageFrameId();
Skate3UltrawideObjectStageRenderer() = renderer;
if (Skate3UltrawideObjectStageTraceEnabled()) {
Skate3UltrawideWriteObjectStageLog(Skate3UltrawideObjectStageFrameId(), "begin", 0,
renderer, 0, 0, site_id, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
}
}
inline uint32_t Skate3UltrawideObjectStageBitForAction(std::string_view action) {
if (action == "render-active-v68") {
return kSkate3ObjectStageActive;
}
if (action == "render-prep-v48") {
return kSkate3ObjectStagePrep;
}
if (action == "render-list-v24") {
return kSkate3ObjectStageList;
}
if (action == "render-main-v52") {
return kSkate3ObjectStageMain;
}
return 0;
}
inline void Skate3UltrawideRecordObjectStage(const char* action, uint32_t site_id,
uint32_t object, uint32_t vtable,
uint32_t target, uint32_t aux0,
uint32_t aux1, uint32_t renderer) {
const uint32_t bit = Skate3UltrawideObjectStageBitForAction(action ? action : "");
if (!bit || !object || !Skate3UltrawideHasExpandedTargetAspect()) {
return;
}
std::lock_guard lock(Skate3UltrawideObjectStageMutex());
auto& record = Skate3UltrawideObjectStageRecords()[object];
if (!record.bits) {
record.first_site = site_id;
record.vtable = vtable;
record.target = target;
}
record.bits |= bit;
record.last_site = site_id;
record.aux0 = aux0;
record.aux1 = aux1;
if (vtable) {
record.vtable = vtable;
}
if (target) {
record.target = target;
}
if (renderer >= 0x10000) {
Skate3UltrawideObjectStageRenderer() = renderer;
}
}
inline uint32_t Skate3UltrawideMaybeForceMainObjectActive(uint32_t site_id,
uint32_t native_value) {
rex::graphics::ultrawide_debug::RecordScreenCallback(
rex::graphics::ultrawide_debug::ScreenCallbackKind::kScreenFlag, site_id, native_value);
if (!Skate3UltrawideHasExpandedTargetAspect() ||
!rex::cvar::Query<bool>("skate3_ultrawide_force_main_visibility_flags")) {
return native_value;
}
rex::graphics::ultrawide_debug::RecordScreenCallbackResult(
rex::graphics::ultrawide_debug::ScreenCallbackKind::kScreenFlag, site_id, native_value, 1);
return 1;
}
inline uint32_t Skate3UltrawideMaybeForceWorldPartVisible(uint32_t site_id,
uint32_t native_value,
uint32_t sibling_value) {
rex::graphics::ultrawide_debug::RecordScreenCallback(
rex::graphics::ultrawide_debug::ScreenCallbackKind::kScreenFlag, site_id, native_value);
if (native_value != 0 || sibling_value == 0 || !Skate3UltrawideHasExpandedTargetAspect() ||
!rex::cvar::Query<bool>("skate3_ultrawide_force_main_visibility_flags")) {
return native_value;
}
rex::graphics::ultrawide_debug::RecordScreenCallbackResult(
rex::graphics::ultrawide_debug::ScreenCallbackKind::kScreenFlag, site_id, native_value, 1);
return 1;
}
inline std::filesystem::path Skate3UltrawideCacheLogPath(const char* filename) {
#if defined(_WIN32)
char* appdata_raw = nullptr;
size_t appdata_length = 0;
if (_dupenv_s(&appdata_raw, &appdata_length, "APPDATA") == 0 && appdata_raw &&
appdata_length > 0) {
std::filesystem::path appdata_path(appdata_raw);
std::free(appdata_raw);
return appdata_path / "skate3" / "cache" / filename;
}
std::free(appdata_raw);
#else
if (const char* appdata = std::getenv("APPDATA")) {
return std::filesystem::path(appdata) / "skate3" / "cache" / filename;
}
#endif
return filename;
}
inline void Skate3UltrawideTraceRenderVirtual(const char* action, uint32_t site_id,
uint32_t object_ptr, uint32_t vtable_ptr,
uint32_t target_ptr, uint32_t aux0 = 0,
uint32_t aux1 = 0, uint32_t renderer = 0) {
Skate3UltrawideRecordObjectStage(action, site_id, object_ptr, vtable_ptr, target_ptr, aux0,
aux1, renderer);
}