// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See ultrawide.h. // // Three mid-asm hooks (config/nfsmw.toml): // - UltrawideProjection, before `stfs f12,0(r30)` at 0x8243EE18 in the view // setup (sub_8243EAC0, CreateViewMatricies in the PC symbols): f12 is the // projection's x scale, P[0][0] = 1 / tan(half the horizontal FOV), // stored once and not used again as a register; f10 is 1 / tan(half the // vertical FOV), which the next instructions negate (`fneg f9,f10`) and // store as P[1][1] (`stfs f9,84(r31)`; the game's y is flipped) and // which is overwritten before it is read again. Narrowing f10's magnitude // by scaleY widens the vertical FOV and leaves the horizontal one. The // view-projection and the frustum planes are built from the stored // matrix afterwards, so culling widens with the view (the top and bottom // planes from its column 1); the LOD distance scale (sub_8243EA08, from // the horizontal FOV only) stays. r28 is the view (the function's second // argument); its id is at +4. Only the views the game itself widens for // 16:9 are changed: 1, 2, 4, 5 and 17 (the player's camera, the road // reflection and the shadow frustum among them), so the shadow maps // (13-16) and the env cube (18-23) keep theirs. // - UltrawideFrontEnd, at the branch at 0x82440E4C that ends the // widescreen case of the front-end matrix (sub_82440D80): f13 is its x // scale (0.92 safe area x 0.75, a 4:3 canvas centred in 16:9) and f0 its // y scale (0.92), stored at m00 and m11 (shader constants c0 and c5). // When the matrix it leaves differs from the last (the window changed // shape), it also clears the game's cached matrix pointer, so the new // matrix is uploaded even when nothing else used the cache since // (game/front_end_matrix.h: the boot movies). // - UltrawideMirror, at the branch at 0x8245CED8 that ends the widescreen // case of the rear-view mirror draw (sub_8245CDF0). The mirror is not a // view: its picture is one quad whose pixel shader (ps_40d7f669a326708d) // samples the car's environment cube map (views 18-23) along per-corner // directions fixed in the vertex data, drawn through a pass-through // vertex shader (vs_58da94362438089c) in clip coordinates the CPU builds // (sub_8245CA98: x = 2 * f1 - 1, width = 2 * f3; 0.638 and -0.275, so x // runs from +0.276 to -0.274, mirrored; y = 2 * f2 - 1, height = 2 * f4). // The front-end matrix never touches it, so without this the picture // stays 16:9-wide while its frame (a HUD sprite) narrows, and the stretch // puts it 1.34x past the frame. Narrowed about the screen centre like the // HUD; the directions stay, so the picture keeps its proportions after // the stretch. // // A ppc/ generated before Vert+ calls the hooks with the x registers only // (the overloads below): Hor+ keeps working and TallHooksActive() stays // false, so the presenter letterboxes a narrower screen as before instead // of the build failing on an undefined symbol. #include #include "ultrawide.h" #include "front_end_matrix.h" #include "glare.h" #include namespace { // Movies (the XDK's XMV player; every file in Movies/ is 16:9 WMV3) are // fitted by the player itself. At each movie's start its renderer setup // (sub_826D8078) loads the display aspect, 16/9, from this .rdata float // (`lfs f0` at 0x826D8114, its only reader) and keeps display aspect / // backbuffer aspect at renderer+140; the auto-fit (sub_826D8640, run from // the format callback sub_826DB140 before the first frame) pillarboxes a // movie narrower than that and letterboxes one wider (loc_826D86C8: the // full width, centred rows). Given the aspect the 1280x720 frame is shown // at, it draws the movie into the centre 1280 * scaleX columns or 720 * // scaleY rows, as the HUD is, so after the presenter's stretch every frame // of it is 16:9 between black bars, with no per-frame decision. constexpr uint32_t kMovieDisplayAspect = 0x8200FAC0; // Both scales in one atomic, so a hook never sees one from before a change // and the other from after it. struct Scales { float x, y; }; std::atomic s_scales{ Scales{ 1.0f, 1.0f } }; std::atomic s_hooksSeen{ false }; std::atomic s_tallHooksSeen{ false }; std::atomic s_frontEndX{ 0.0f }; std::atomic s_frontEndY{ 0.0f }; // Log each view id once, with its target size (the per-view table at // 0x82C3F590 + id * 384: target pointer at +368, width/height at +16/+20). void LogView(uint32_t id, bool widened) { static std::atomic seen{ 0 }; if (id >= 64 || (seen.fetch_or(1ull << id) & (1ull << id))) return; uint32_t target = ByteSwap(*static_cast(g_memory.Translate(0x82C3F590 + id * 384 + 368))); uint32_t w = 0, h = 0; if (target) { w = ByteSwap(*static_cast(g_memory.Translate(target + 16))); h = ByteSwap(*static_cast(g_memory.Translate(target + 20))); } fprintf(stderr, "[uw] view %u: target %ux%u%s\n", id, w, h, widened ? ", widened" : ""); } // The view at `view` (guest address) is one the game widens for 16:9 // (game/glare.h keeps the set: the sun's glow is fitted in the same views). bool WidenedView(uint32_t view) { uint32_t id = ByteSwap(*static_cast(g_memory.Translate(view + 4))); bool widen = game::glare::WidenedViewId(id); LogView(id, widen); return widen; } // `stfs` stores the register as a float. void ScaleRegister(PPCRegister& f, float scale) { f.f64 = double(float(f.f64) * scale); } // About the screen centre (0.5 of it), for the mirror's 0..1 edge. void ScaleAboutCentre(PPCRegister& f, float scale) { f.f64 = double(0.5f + (float(f.f64) - 0.5f) * scale); } // The front-end matrix the hook left last (the game's render thread), // and NFSMW_FE_RESEND=0 to leave the game's matrix cache alone (the // matrix then stays narrowed for an old window shape through the boot // movies, as before; an A/B switch). game::front_end::MatrixTracker s_frontEndMatrix; const bool s_frontEndResend = [] { const char* v = std::getenv("NFSMW_FE_RESEND"); return !(v && v[0] == '0'); }(); // The hook changed the front end's matrix (a new window shape, or the // first frames): clear sub_82453768's cached matrix pointer so the call // right after the hook uploads it (game/front_end_matrix.h). void ResendIfChanged(float x, float y) { if (!s_frontEndMatrix.Changed(x, y) || !s_frontEndResend) return; *static_cast(g_memory.Translate(game::front_end::kMatrixCachePointer)) = 0; static uint32_t logged = 0; if (++logged <= 16) fprintf(stderr, "[uw] front-end matrix x %.4f y %.4f: sent to the game again%s\n", x, y, logged == 16 ? " (no more of these lines)" : ""); } } namespace game { void SetAspectScale(float scaleX, float scaleY) { const Scales next{ std::clamp(scaleX, 0.1f, 1.0f), std::clamp(scaleY, 0.5f, 1.0f) }; const Scales last = s_scales.exchange(next, std::memory_order_relaxed); if (last.x != next.x || last.y != next.y) fprintf(stderr, "[uw] view scale x %.4f y %.4f\n", next.x, next.y); // The movie player's display aspect, stored with every decision (one // 4-byte store per presented frame) so nothing depends on when the // image was loaded. Read once per movie, at its start: a change made // while a movie plays applies from the next movie. float aspect = (16.0f / 9.0f) * next.y / next.x; uint32_t bits; memcpy(&bits, &aspect, sizeof(bits)); *static_cast(g_memory.Translate(kMovieDisplayAspect)) = ByteSwap(bits); } float AspectScaleX() { return s_scales.load(std::memory_order_relaxed).x; } float AspectScaleY() { return s_scales.load(std::memory_order_relaxed).y; } bool HooksActive() { return s_hooksSeen.load(std::memory_order_relaxed); } bool TallHooksActive() { return s_tallHooksSeen.load(std::memory_order_relaxed); } float FrontEndScaleX() { return s_frontEndX.load(std::memory_order_relaxed); } float FrontEndScaleY() { return s_frontEndY.load(std::memory_order_relaxed); } } void UltrawideProjection(PPCRegister& r28, PPCRegister& f12, PPCRegister& f10) { if (!s_tallHooksSeen.load(std::memory_order_relaxed)) { s_hooksSeen.store(true, std::memory_order_relaxed); s_tallHooksSeen.store(true, std::memory_order_relaxed); } const Scales scale = s_scales.load(std::memory_order_relaxed); if (scale.x == 1.0f && scale.y == 1.0f) return; if (!WidenedView(r28.u32)) return; if (scale.x != 1.0f) ScaleRegister(f12, scale.x); if (scale.y != 1.0f) ScaleRegister(f10, scale.y); // its magnitude: negated into P[1][1] after this } void UltrawideFrontEnd(PPCRegister& f13, PPCRegister& f0) { const Scales scale = s_scales.load(std::memory_order_relaxed); if (scale.x != 1.0f) ScaleRegister(f13, scale.x); if (scale.y != 1.0f) ScaleRegister(f0, scale.y); s_frontEndX.store(float(f13.f64), std::memory_order_relaxed); s_frontEndY.store(float(f0.f64), std::memory_order_relaxed); ResendIfChanged(float(f13.f64), float(f0.f64)); } void UltrawideMirror(PPCRegister& f1, PPCRegister& f2, PPCRegister& f3, PPCRegister& f4) { const Scales scale = s_scales.load(std::memory_order_relaxed); if (scale.x != 1.0f) { ScaleAboutCentre(f1, scale.x); ScaleRegister(f3, scale.x); } if (scale.y != 1.0f) { ScaleAboutCentre(f2, scale.y); ScaleRegister(f4, scale.y); } } // The hooks as a ppc/ generated before Vert+ calls them: x only. void UltrawideProjection(PPCRegister& r28, PPCRegister& f12) { if (!s_hooksSeen.load(std::memory_order_relaxed)) s_hooksSeen.store(true, std::memory_order_relaxed); const float scale = s_scales.load(std::memory_order_relaxed).x; if (scale == 1.0f) return; if (WidenedView(r28.u32)) ScaleRegister(f12, scale); } void UltrawideFrontEnd(PPCRegister& f13) { const float scale = s_scales.load(std::memory_order_relaxed).x; if (scale != 1.0f) ScaleRegister(f13, scale); s_frontEndX.store(float(f13.f64), std::memory_order_relaxed); ResendIfChanged(float(f13.f64), 0.0f); } void UltrawideMirror(PPCRegister& f1, PPCRegister& f3) { const float scale = s_scales.load(std::memory_order_relaxed).x; if (scale == 1.0f) return; ScaleAboutCentre(f1, scale); ScaleRegister(f3, scale); }