From 49fb08dad1ec46b9b5093231ed55a14c32d3166c Mon Sep 17 00:00:00 2001 From: knuckleslee <31877375+knuckleslee@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:27:47 +0800 Subject: [PATCH] Ultrawide: stretch the menus' 3D-to-layout projection by the camera's aspect The ring menu places the selected item's quad and tests the hand cursor against its items through two projection functions that map a 3D point onto the 1280x720 layout with the constants 640 and 360. With a wide frame's camera their x came out 1/k as far from the layout's middle as the GPU draws the ring (k: the camera's aspect over 16:9), so the selected item was drawn too narrow and the cursor had to be moved closer to the middle than the item looked to select it. Their x results are now stretched about 640 by k, read from the camera's own matrix, in the menus and in a race's pause menu. A 16:9 camera reads k = 1 and is left alone. See docs/main-menu.md. Co-Authored-By: Claude Sonnet 5.5 --- docs/main-menu.md | 12 ++++++ src/game_patches.cpp | 90 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/docs/main-menu.md b/docs/main-menu.md index 5ea77e2c..df5e181d 100644 --- a/docs/main-menu.md +++ b/docs/main-menu.md @@ -77,3 +77,15 @@ set SFR_INPUT_SCRIPT=back@0+0.2,rright@1+0.03,rdown@3+0.19,rright@6+0.16,rup@6.5 - 沒有玩家設定檔,因此無法存檔。 - 部分頂點著色器仍無法翻譯(頂點取用超出宣告),相關 3D 物件不會畫出。 - 一位元組紋素的 tiled 貼圖、其他 swizzle 的 8888 格式尚未支援。 + +## 超寬畫面的選單環 + +超寬視窗(比 16:9 更寬)時,遊戲的相機長寬比是整個畫面的比例,選單環的圖示由 GPU 畫在整個寬畫面上。 +但遊戲自己把 3D 位置換成 1280x720 版面像素的兩個函式(`8226CB10` 用標題的相機、`823F18D0` 用呼叫者給的相機) +算出來的 x 只有 16:9 時的 1/k(k 是相機長寬比除以 16:9),以版面中心 640 為基準。 +結果是:被指到的圖示畫得太窄、位置偏向中央;游標要比圖示看起來更靠近中央才選得到;返回鍵約在畫出來的位置左邊 300 px 才選得到。 +16:9 的同一個選單把被選中的圖示放在 x = 847、261 寬,超寬時是 709、87 寬。 + +`game_patches.cpp` 在這兩個函式的出口把 x 以 640 為基準放大 k 倍。k 從相機自己的矩陣(x 與 y 兩欄前三項的長度)讀出, +相機本來就是 16:9 時 k = 1,不動。生效範圍:選單的 1280 寬畫面;比賽中(畫面和比賽 HUD 另有安排)只限暫停選單的環與通用元件 +(`823F18D0` 從 `8246B338`、`823F2318`、`823F63F8` 的呼叫點)。 diff --git a/src/game_patches.cpp b/src/game_patches.cpp index e51434a7..e8517c64 100644 --- a/src/game_patches.cpp +++ b/src/game_patches.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -80,6 +81,95 @@ SFR_CONCURRENT_HOOK(sub_8222B058) { sfr::enter_function(ctx,"sub_8222B058",0x822 SFR_CONCURRENT_HOOK(sub_82242AB8) { sfr::enter_function(ctx,"sub_82242AB8",0x82242AB8); refresh_camera_aspect(); __imp__sub_82242AB8(ctx,base); } SFR_CONCURRENT_HOOK(sub_823F29D8) { sfr::enter_function(ctx,"sub_823F29D8",0x823F29D8); refresh_camera_aspect(); __imp__sub_823F29D8(ctx,base); } +PPC_FUNC_IMPL(__imp__sub_8226CB10); +PPC_FUNC_IMPL(__imp__sub_823F18D0); + +// The menus' ring and its hit test, on a wide frame. +// +// The title turns a 3D position into a point of its 1280x720 layout with two +// functions: 8226CB10 (the camera is the one in the title's services) and +// 823F18D0 (a camera given by the caller). Each takes the point through the +// camera's matrix (at +352), divides by w and maps the result onto the layout +// with 640 and 360 (the globals at 82B34BD8/82B34BDC). The ring menu places the +// selected item's quad with them and asks them where each item is when it +// tests whether the hand cursor points at it. +// +// With the wide frame's camera (its aspect is the frame's, see +// refresh_camera_aspect) the same normalized x covers k times more of the +// layout than 640 of it, which is where the GPU draws the ring: in the layout +// the item came out k times too close to its middle. The selected item was +// drawn k times too narrow, and the cursor had to be moved k times closer to +// the middle than the item looked to select it; the Back button was selected +// about 300 px left of where it is drawn. A 16:9 run of the same menu puts the +// selected item at x = 847 and 261 wide, where the wide frame gave 709 and 87. +// +// Both functions' x results are stretched about 640 by k, the camera's aspect +// over 16:9 read from its own matrix (the lengths of its x and y columns' +// first three entries). A camera that is 16:9 already reads k = 1 and is left +// alone, as is everything outside the wide mode. +// +// Where: the menus' 1280-wide view; in a race (the view is as wide as the frame +// and its HUD is placed by other means) only the pause menu's ring and widgets: +// 823F18D0 called from the ring's point-in-item test (8246B338), from the +// generic widget's textured quad (823F2318) and from its corner test (823F63F8). +static double projection_stretch(uint32_t camera,bool ring_in_race=false) { + if(!wide_logical_width() || !sfr::active_guest_graphics) return 0.0; + auto& memory=*sfr::active_memory; + const bool racing=memory.load(0x83E52F8C)!=0; + if(racing ? !ring_in_race : sfr::active_guest_graphics->last_view_width()!=sfr::aspect::title_layout_width) return 0.0; + if(!camera || !memory.readable(uint64_t(camera)+352,64)) return 0.0; + const auto element=[&](uint32_t row,uint32_t column) { + return double(std::bit_cast(memory.load(uint64_t(camera)+352+16*row+4*column))); + }; + double x=0.0, y=0.0; + for(uint32_t row=0;row<3;++row) { x+=element(row,0)*element(row,0); y+=element(row,1)*element(row,1); } + x=std::sqrt(x); y=std::sqrt(y); + if(x<1e-9) return 0.0; + const double k=(y/x)/(double(sfr::aspect::title_layout_width)/double(sfr::aspect::title_layout_height)); + return k>1.25 && k<12.0 ? k : 0.0; +} + +SFR_CONCURRENT_HOOK(sub_8226CB10) { + sfr::enter_function(ctx,"sub_8226CB10",0x8226CB10); + const uint32_t out=ctx.r3.u32; + __imp__sub_8226CB10(ctx,base); + if(!wide_logical_width()) return; + auto& memory=*sfr::active_memory; + const auto word=[&](uint64_t address) { return memory.readable(address,4) ? memory.load(address) : 0u; }; + // The camera the function reads: [[[[83E516A0]+116]+8]+4*[[83E516A0]+116]+28]]. + const uint32_t services=word(0x83E516A0), table=services ? word(uint64_t(services)+116) : 0; + const uint32_t cameras=table ? word(uint64_t(table)+8) : 0, index=table ? word(uint64_t(table)+28) : 0; + const uint32_t camera=cameras ? word(uint64_t(cameras)+4ull*index) : 0; + const double k=projection_stretch(camera); + if(k==0.0 || !memory.readable(out,8)) return; + const double centre=double(std::bit_cast(word(0x82B34BD8))); + if(centre<=0.0) return; + const double value=double(std::bit_cast(word(out))); + memory.store(out,std::bit_cast(float(centre+(value-centre)*k))); +} + +SFR_CONCURRENT_HOOK(sub_823F18D0) { + sfr::enter_function(ctx,"sub_823F18D0",0x823F18D0); + const uint32_t camera=ctx.r3.u32; + // The call sites a race's pause menu uses: 8246B338's four corner tests, + // 823F2318's four quad corners, 823F63F8's four corner tests. + const uint32_t site=uint32_t(ctx.lr); + const bool ring_test=site==0x8246B3A0u || site==0x8246B3E4u || site==0x8246B428u || site==0x8246B46Cu || + site==0x823F2414u || site==0x823F2510u || site==0x823F2724u || site==0x823F2768u || + site==0x823F6554u || site==0x823F65DCu || site==0x823F6664u || site==0x823F64CCu; + __imp__sub_823F18D0(ctx,base); + // The result is the x and y floats in r3's high and low words. + if(const double k=projection_stretch(camera,ring_test); k!=0.0) { + const uint64_t packed=ctx.r3.u64; + const double centre=double(std::bit_cast(sfr::active_memory->load(0x82B34BD8))); + if(centre>0.0) { + const double x=double(std::bit_cast(uint32_t(packed>>32))); + const uint32_t stretched=std::bit_cast(float(centre+(x-centre)*k)); + ctx.r3.u64=(uint64_t(stretched)<<32)|(packed&0xFFFFFFFFull); + } + } +} + PPC_FUNC_IMPL(__imp__sub_8249B068); // Graphics object initialization. It copies the presentation parameters it is