#pragma once #include #include #include #include #include #include #include // 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("skate3_ultrawide") || !rex::graphics::IsNativeGuestOutputActive()) { return false; } return rex::cvar::Query("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("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("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; };