#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include inline bool Skate3UltrawideHasExpandedTargetAspect() { if (!rex::cvar::Query("skate3_ultrawide") || !rex::cvar::Query("skate3_ultrawide_hor_plus") || !rex::graphics::ultrawide_debug::IsSkate3GameplayUltrawideActive()) { return false; } return rex::cvar::Query("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("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("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("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("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("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("skate3_ultrawide_force_main_visibility_flags")) { return native_value; } return 1; } inline bool Skate3UltrawideShouldForceWorldMainCullDescriptor(u8* base, uint32_t descriptor) { if (!Skate3UltrawideHasExpandedTargetAspect() || !rex::cvar::Query("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("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("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; using PlaneBits = std::array; struct WidenedPlaneCache { uint32_t block = 0; std::array original{}; std::array 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 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& Skate3UltrawideObjectStageRecords() { static std::unordered_map 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("skate3_ultrawide_object_stage_trace_continuous") || rex::cvar::Query("skate3_ultrawide_object_stage_trace_frames_remaining") > 0 || rex::cvar::Query("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("skate3_ultrawide_object_stage_trace_to_disk")) { return; } static std::atomic 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(rex::cvar::Query("skate3_ultrawide_object_stage_marker")); const bool enabled = Skate3UltrawideObjectStageTraceEnabled(); uint32_t remaining = static_cast(std::max(0, rex::cvar::Query( "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(records.size())); uint32_t emitted_missing = 0; const uint32_t missing_limit = static_cast(std::clamp( rex::cvar::Query("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(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("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("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); }