# Code patches applied by the recompiler (tools/recomp/gcrecomp.py): ADDRESS before|after C-STATEMENT.
# Addresses are this build's: main.dol as is, each REL at its own fixed base (to be decided in Phase 0;
# the Riders port linked its single REL at REL_BASE 0x81800000).
# Empty in the fork: Sonic Heroes' patches (widescreen, diagnostics) were removed.

# ---- Widescreen (any aspect), Hor+: RenderWare's RwCameraSetViewWindow (fn_8019D0CC [likely]: copies the view
# window into camera+0x68/0x6C, stores the reciprocals at 0x70/0x74, then updates the frustum) widens the view
# window by g_aspect_scale (target aspect / (4/3)) right after the copy, so projection and culling both follow.
# Only cameras drawing into a screen-sized frame buffer (raster width at +0xC >= 512): the 128 x 128
# render-to-texture cameras (reflections) keep theirs. 2026-11-11.
# The game sets the stage camera's window every frame [confirmed PC log, 2026-11-12], but the HUD bakes the
# widened window into its objects when the stage loads, so a screen change during a stage waits for the next
# stage (settings.c settings_hold_view, rel.c).
# Stage cameras only (rel_stage_active: a stageNND REL is in): the menus' 2D is stretched to the width, so their
# 3D (character select models, advertiseD's camera, its window set once as the module loads) must stay unwidened
# to line up with it; widened, the models sat inside the panels' 4:3 places after a stage (Switch, Fit, 2026-11-12).
decl extern volatile double g_aspect_scale;
decl int rel_stage_active(void);
8019D0F0 before { double k = g_aspect_scale; uint32_t fb = ld32(c->r[3] + 0x60); if (k != 1.0 && rel_stage_active() && fb && (int32_t)ld32(fb + 0xC) >= 512) st32(c->r[3] + 0x68, f32_as_u32((float)(u32_as_f32(ld32(c->r[3] + 0x68)) * k))); }

# ---- Camera (docs/notes/re/camera.md; settings cam_speed and modern_controls, 2026-11-11).
# Turn speed: the follow cameras turn by 5 x the analog trigger per frame (mulli 5 in SARATOGA fn_801348D0 and
# JONES fn_80131600, one per trigger); the result is scaled by g_cam_turn_scale (setting cam_speed / 100).
decl extern volatile double g_cam_turn_scale;
80134F34 after { if (g_cam_turn_scale != 1.0) c->r[0] = (uint32_t)(int32_t)((int32_t)c->r[0] * g_cam_turn_scale); }
80134F44 after { if (g_cam_turn_scale != 1.0) c->r[3] = (uint32_t)(int32_t)((int32_t)c->r[3] * g_cam_turn_scale); }
80131A30 after { if (g_cam_turn_scale != 1.0) c->r[0] = (uint32_t)(int32_t)((int32_t)c->r[0] * g_cam_turn_scale); }
80131A40 after { if (g_cam_turn_scale != 1.0) c->r[3] = (uint32_t)(int32_t)((int32_t)c->r[3] * g_cam_turn_scale); }
# Tilt (modern controls: the right stick's up/down, g_cam_pitch_in from si.c), as an orbit around the camera's
# look-at point. The game keeps no pitch between frames (fn_800516E4 recomputes it from the eye -> look-at
# vector), so the tilt goes into the view only: fn_8004E7EC copies the eye to the block r3 and calls fn_80050AB8
# (8004EA74), which builds the stage camera's frame (pitch in degrees in f1 at 80050B78; then it moves the frame to
# the block's eye). Here the block's eye is swung up/down about the look-at point (the per-screen camera copy at
# *0x8042C208: +0x24 look-at) by the tilt, elevation kept within -5..75 deg (lower went under the floor), and the view pitched by the same
# angle, so the team stays in the middle. The tilt grows ~2 deg/frame at full stick and 100% speed, +-40 deg, and
# stays where it is left. Gameplay state is untouched. [confirmed PC 2026-11-11 for the view pitch; orbit: see
# HISTORY] (docs/notes/re/camera.md)
decl extern volatile int g_cam_pitch_in;
decl extern volatile double g_cam_pitch_deg, g_cam_view_add;
decl extern volatile int g_cam_tilt_on;
8004EA74 before { double o = g_cam_pitch_deg + g_cam_pitch_in * (2.0 / 127.0) * g_cam_turn_scale; o = o > 40.0 ? 40.0 : o < -40.0 ? -40.0 : o; g_cam_pitch_deg = o; g_cam_view_add = 0.0; g_cam_tilt_on = 1; if (o != 0.0) { uint32_t cp = ld32(0x8042C208u), b = c->r[3]; double tx = ldf32(cp + 0x24), ty = ldf32(cp + 0x28), tz = ldf32(cp + 0x2C); double vx = ldf32(b) - tx, vy = ldf32(b + 4) - ty, vz = ldf32(b + 8) - tz; double h = __builtin_sqrt(vx * vx + vz * vz), dist = __builtin_sqrt(h * h + vy * vy); if (h > 0.001) { double e = __builtin_atan2(vy, h) * 57.29578, e2 = e - o; if (e2 > 75.0) e2 = e > 75.0 ? e : 75.0; if (e2 < -5.0) e2 = e < -5.0 ? e : -5.0; double k = dist * __builtin_cos(e2 / 57.29578) / h; stf32(b, tx + vx * k); stf32(b + 4, ty + dist * __builtin_sin(e2 / 57.29578)); stf32(b + 8, tz + vz * k); g_cam_view_add = e - e2; } } }
8004EA74 after { g_cam_tilt_on = 0; }
80050B78 after { if (g_cam_tilt_on) c->f[1].ps0 += g_cam_view_add; }

# ---- Character shadows (docs/notes/re/rw-gcn-raster.md, 2026-11-12). fn_800B7C88 draws each team member's soft
# shadow by EFB copies into two 128x128 textures and locks both for WRITE first (RwRasterLock fn_801A1AC8, flags
# 0x9 / 0xB) only to wait until the GP has finished sampling them; the CPU never touches the pixels. Our GP thread
# runs the FIFO in order and keeps copies GPU-side, so the wait protects nothing, and it drained the whole GP queue
# 6 times a frame (1.5-2.6 ms/frame on the Switch). The WRITE bit is dropped: no wait, the lock still happens.
800B7CE4 before { c->r[5] &= ~1u; }
800B7E70 before { c->r[5] &= ~1u; }
