From 39656787af7414def0a938d86e555d4322d57bc0 Mon Sep 17 00:00:00 2001 From: BitJager <6281442+filipverlee@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:52:49 +0200 Subject: [PATCH 1/7] Fix Evo's visor floating above his head in rig cutscenes render_rig_instance_xlu pushed joint 1 a second time before joint 3, so rig part 9 (Evo's red visor, and Dan's part 9) got the joint 1 offset twice and floated a head-height above the model in the intro cockpit. Push joint 3 from the joint 1 level restored by the preceding pop and drop the now-unneeded pop, keeping the stack balanced. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01421MLZmp5pdkDs5MnD9xmK --- patches/modelview_stack_patches.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/patches/modelview_stack_patches.c b/patches/modelview_stack_patches.c index d5d1236..bd43659 100644 --- a/patches/modelview_stack_patches.c +++ b/patches/modelview_stack_patches.c @@ -104,7 +104,9 @@ RECOMP_PATCH void render_rig_instance_xlu(s32 x, s32 y, s32 z, s16 pitch, s16 ya gSPPopMatrix(gXluDL++, G_MTX_MODELVIEW); } - append_joint_pose_transform(1, &gRigPoseStates[pose_bank][1]); + // The original pushes joint 1 a second time here, so part 9 gets the joint 1 offset twice: + // Evo's visor floats a head-height above him in the intro cockpit. The pop above already + // restored the joint 1 level, so push joint 3 from there. if (pose_bank == 1) { gDPSetPrimColor(gXluDL++, 0, 0, 0xEA, 0xE6, 0xFF, 0xFF); @@ -122,7 +124,6 @@ RECOMP_PATCH void render_rig_instance_xlu(s32 x, s32 y, s32 z, s16 pitch, s16 ya gSPDisplayList(gXluDL++, gRigPartDisplayLists[rig_variant][9]); gSPPopMatrix(gXluDL++, G_MTX_MODELVIEW); gSPPopMatrix(gXluDL++, G_MTX_MODELVIEW); - gSPPopMatrix(gXluDL++, G_MTX_MODELVIEW); // Return to the caller's stack depth before the next translucent draw. gSPPopMatrix(gXluDL++, G_MTX_MODELVIEW); From 72575151fefb8c3c5e676ebb72739de90e9c3cd6 Mon Sep 17 00:00:00 2001 From: BitJager <6281442+filipverlee@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:52:49 +0200 Subject: [PATCH 2/7] Restore TV-safe scissor after rectangles and for the title logo ship draw_rectangle left the scissor at full screen and draw_intro_logo_model always used the full frame, so in 4:3 the 3D geometry drawn afterwards (and the title-screen ship) spilled into the 8px overscan border. The original restores 8, 8, w - 8, h - 8; use set_default_frame_scissor, which keeps the full frame in widescreen. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01421MLZmp5pdkDs5MnD9xmK --- patches/graphics_patches.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/patches/graphics_patches.c b/patches/graphics_patches.c index f98d8ab..8f12c6d 100644 --- a/patches/graphics_patches.c +++ b/patches/graphics_patches.c @@ -477,7 +477,9 @@ RECOMP_PATCH void draw_rectangle(Gfx** dl, s16 x0, s16 y0, s16 x1, s16 y1, u8 r, gDPFillRectangle((*dl)++, draw_x0, draw_y0, draw_x1, draw_y1); } - gDPSetScissor((*dl)++, G_SC_NON_INTERLACE, 0, 0, screen_w, screen_h); + // The original restores the TV-safe frame scissor (8, 8, w - 8, h - 8) here; leaving it at + // full screen lets the 3D geometry drawn afterwards spill into the overscan border. + set_default_frame_scissor(dl); gDPPipeSync((*dl)++); g_dbg_draw_rectangle_calls++; @@ -597,11 +599,9 @@ static inline void append_intro_black_border_mask(Gfx** dl) { } RECOMP_PATCH void draw_intro_logo_model(void) { - ScissorRect full_rect; - - fill_full_scissor_rect(&full_rect); - - gDPSetScissor(gMainDL++, G_SC_NON_INTERLACE, full_rect.ulx, full_rect.uly, full_rect.lrx, full_rect.lry); + // The original clips the logo ship to the TV-safe frame (8, 8, 312, 232); only widescreen + // may open it up to the full frame. + set_default_frame_scissor(&gMainDL); gSPDisplayList(gMainDL++, gOpaqueLit3DRenderSetupDl); guTranslate( From e9f8d40c14fa382a69495efc40cdf2a9ec88852c Mon Sep 17 00:00:00 2001 From: BitJager <6281442+filipverlee@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:17:36 +0200 Subject: [PATCH 3/7] Stop the camera shaking: keep the game's own visibility results With SSSV_PATCH_DISABLE_LOD/DISABLE_FAR_AWAY_CULL the visibility classifiers returned VISIBILITY_VISIBLE for everything in a loaded cell. The game acts on those results beyond rendering, and the camera never settles: at rest it cycles +1.5, +1.6, +1.8, -3.9 every 6-7 frames (139 direction reversals in 900 frames at the level 1 start), which shows as big camera jumps while moving. On the original (Mupen64Plus) the camera is still at rest. Split the overrides: DISABLE_LOD now only forces gLodDetailState = 0 for objects the game itself classified as visible, and the far-away cull override (unchanged behaviour) is off by default. Same spot after the change: 0 reversals, camera at rest. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01421MLZmp5pdkDs5MnD9xmK --- patches/lod_patches.c | 26 +++++++++++++++++++------- patches/sssv_patch_types.h | 2 +- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/patches/lod_patches.c b/patches/lod_patches.c index 1e07f0b..168c85f 100644 --- a/patches/lod_patches.c +++ b/patches/lod_patches.c @@ -1,14 +1,26 @@ #include "sssv_patch_common.h" #include "sssv_render_context.h" -static inline void apply_lod_overrides(s32* arg3) { - -#if SSSV_PATCH_DISABLE_LOD || SSSV_PATCH_DISABLE_FAR_AWAY_CULL +// Forcing every object to VISIBILITY_VISIBLE changes more than rendering: the game also acts on the +// out-of-bounds/too-far results, and the camera oscillates (it never settles, so it shakes while the +// player moves). The far-away cull override therefore stays off by default; the LOD override only +// raises the detail level of objects the game itself classified as visible. +static inline void apply_far_away_cull_override(s32* arg3) { +#if SSSV_PATCH_DISABLE_FAR_AWAY_CULL *arg3 = 0; gLodDetailState = 0; #endif } +static inline s16 apply_lod_override(s16 visibility) { +#if SSSV_PATCH_DISABLE_LOD + if (visibility == VISIBILITY_VISIBLE) { + gLodDetailState = 0; + } +#endif + return visibility; +} + RECOMP_PATCH s16 classify_object_visibility_6FA0A0( s32 xPos, s32 zPos, @@ -22,7 +34,7 @@ RECOMP_PATCH s16 classify_object_visibility_6FA0A0( u8 arg9 ) { rc_set(RC_WORLD3D); - apply_lod_overrides(&arg3); + apply_far_away_cull_override(&arg3); if (arg3 == 0) { if (is_world_cell_loaded_6AB9E4(xPos >> 16, zPos >> 16, yPos >> 16) != 0) { @@ -32,7 +44,7 @@ RECOMP_PATCH s16 classify_object_visibility_6FA0A0( } } - return classify_visibility_and_draw_fov_mask(xPos, zPos, yPos, arg3, fovImageIdx, arg5, arg6, arg7, arg8, arg9); + return apply_lod_override(classify_visibility_and_draw_fov_mask(xPos, zPos, yPos, arg3, fovImageIdx, arg5, arg6, arg7, arg8, arg9)); } RECOMP_PATCH s16 classify_dynamic_visibility_6FA26C( @@ -48,7 +60,7 @@ RECOMP_PATCH s16 classify_dynamic_visibility_6FA26C( u8 arg9 ) { rc_set(RC_WORLD3D); - apply_lod_overrides(&arg3); + apply_far_away_cull_override(&arg3); if (arg3 == 0) { if (is_world_cell_loaded_6AB9E4(xPos >> 16, zPos >> 16, yPos >> 16) != 0) { @@ -58,5 +70,5 @@ RECOMP_PATCH s16 classify_dynamic_visibility_6FA26C( } } - return classify_visibility_and_draw_fov_mask(xPos, zPos, yPos, arg3, fovImageIdx, arg5, arg6, arg7, arg8, arg9); + return apply_lod_override(classify_visibility_and_draw_fov_mask(xPos, zPos, yPos, arg3, fovImageIdx, arg5, arg6, arg7, arg8, arg9)); } diff --git a/patches/sssv_patch_types.h b/patches/sssv_patch_types.h index f4bee54..0e562b9 100644 --- a/patches/sssv_patch_types.h +++ b/patches/sssv_patch_types.h @@ -18,7 +18,7 @@ #define VISIBILITY_INVISIBLE 4 #define SSSV_PATCH_DISABLE_LOD 1 -#define SSSV_PATCH_DISABLE_FAR_AWAY_CULL 1 +#define SSSV_PATCH_DISABLE_FAR_AWAY_CULL 0 #define SSSV_PATCH_TRANSPARENT_SHIP_GLASS 0 #define SSSV_PATCH_DISABLE_PB_UFO_GLASS_OBJECT 0 #define SSSV_PATCH_SHIP_WINDOW_XLU_STATE_BARRIER 1 From 0ebe0a14e270bf1c9e0b9ad50bbc73e3dee752fe Mon Sep 17 00:00:00 2001 From: BitJager <6281442+filipverlee@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:13:03 +0200 Subject: [PATCH 4/7] Only render to RDRAM when the game reads the frame back RT64 writes every frame back to RDRAM by default. That readback scales with the output resolution: at 2x on an Intel Iris Plus 640 the gameplay attract demo ran at 24.9 fps with 357 of 870 frames over 40 ms. The game only reads rendered frames in two places: the title/attract screen grab for the transition into the game, and the previous frame copied into the pause-menu thumbnail (gInitialisationState 1-3). Emit gEXSetRenderToRAM(0) for gameplay frames, except while the pause menu is being entered, and gEXSetRenderToRAM(1) for intro/title frames. Same demo afterwards: 29.8 fps, 87 frames over 40 ms (same as turning render-to-RAM off globally). Title transition and pause thumbnail checked on screen. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01421MLZmp5pdkDs5MnD9xmK --- patches/graphics_patches.c | 9 +++++++++ patches/sssv_patch_symbols.h | 1 + 2 files changed, 10 insertions(+) diff --git a/patches/graphics_patches.c b/patches/graphics_patches.c index 8f12c6d..79b6a26 100644 --- a/patches/graphics_patches.c +++ b/patches/graphics_patches.c @@ -732,6 +732,10 @@ RECOMP_PATCH void render_intro_overlay_frame_63BF88(void) { gScreenWidth = target_width; gScreenHeight = SSSV_BASE_HEIGHT; load_segments(&gMainDL, gDisplayListContext); + + // Title and attract frames are grabbed from RDRAM for the transition into the game. + rc_ensure_rt64_extended_enabled(&gMainDL); + gEXSetRenderToRAM(gMainDL++, 1); clear_depth_buffer(&gMainDL); write_main_viewport_expand(gScreenWidth, gScreenHeight); @@ -802,6 +806,11 @@ RECOMP_PATCH void setup_world_perspective_6AB090(DisplayList* arg0) { rc_ensure_rt64_extended_enabled(&gLayer0DL); rc_ensure_rt64_extended_enabled(&gAuxDL); + // Writing every rendered frame back to RDRAM costs a GPU readback that scales with the output + // resolution (2x: ~25 fps instead of 30 on weaker GPUs). In gameplay the game only reads a frame + // back while entering the pause menu, where it copies the previous frame into the menu thumbnail. + gEXSetRenderToRAM(gMainDL++, (gInitialisationState != 0) ? 1 : 0); + if (target_width > SSSV_BASE_WIDTH) { append_world_depth_clear_failsafe(); } diff --git a/patches/sssv_patch_symbols.h b/patches/sssv_patch_symbols.h index 373d61f..b77f783 100644 --- a/patches/sssv_patch_symbols.h +++ b/patches/sssv_patch_symbols.h @@ -20,6 +20,7 @@ extern u8 gLodDetailState; extern s16 gScreenWidth; extern s16 gScreenHeight; extern s8 gAttractModeState; +extern u8 gInitialisationState; extern VpRaw gMainViewport; extern Gfx* gMainDL; extern Gfx* gOpaqueDL; From ae752ad47e8acb5c61776d10bb9c6bdee48ffb39 Mon Sep 17 00:00:00 2001 From: BitJager <6281442+filipverlee@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:26:08 +0200 Subject: [PATCH 5/7] Show the static on TV screen monitors The TV screen model draws its static as a white quad with prim alpha 0x80 and G_AC_DITHER, in 2-cycle mode with G_CC_PASS2 / G_CC_PRIMITIVE. The alpha compare uses the first cycle's alpha, which is COMBINED there. On hardware that still holds the previous pixel's output, so about half the pixels pass. RT64 starts COMBINED at 0, every pixel fails and the monitors in Something Fishy stay black. Rewrite that combine command in the loaded model to use PRIMITIVE in both cycles, only when segment 1 holds the TV model. Co-Authored-By: Claude Opus 5.5 --- patches/graphics_patches.c | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/patches/graphics_patches.c b/patches/graphics_patches.c index 79b6a26..dc4f3ab 100644 --- a/patches/graphics_patches.c +++ b/patches/graphics_patches.c @@ -257,6 +257,35 @@ static inline void patch_world_chunk_clip_ratio_dl(void) { } } +// The TV screen model (Something Fishy monitors) draws its static as a white quad with prim alpha 0x80 +// and G_AC_DITHER, in 2-cycle mode with G_CC_PASS2 / G_CC_PRIMITIVE. The alpha compare uses the first +// cycle's alpha, which there is COMBINED: on hardware that still holds the previous pixel's output (~0x80), +// so about half the pixels pass at random. RT64 starts COMBINED at 0, every pixel fails and the screen +// stays black. Use PRIMITIVE in the first cycle as well so the dither compares against the prim alpha. +#define TV_SCREEN_DL_SEGMENT_OFFSET 0x00017AA0u +#define TV_SCREEN_STATIC_COMBINE_INDEX 27 + +static inline void patch_tv_screen_static_dl(void) { + Gfx* cmd; + Gfx original; + Gfx fixed; + + if (gSegment1Base == NULL) { + return; + } + + cmd = &((Gfx*)((u8*)gSegment1Base + TV_SCREEN_DL_SEGMENT_OFFSET))[TV_SCREEN_STATIC_COMBINE_INDEX]; + gDPSetCombineMode(&original, G_CC_PASS2, G_CC_PRIMITIVE); + gDPSetCombineMode(&fixed, G_CC_PRIMITIVE, G_CC_PRIMITIVE); + + // Only touch it while segment 1 holds the TV model (the previous command is its dithered render mode). + if ((cmd->words.w0 == original.words.w0) && (cmd->words.w1 == original.words.w1) && + ((cmd[-1].words.w0 & 0xFF000000u) == 0xB9000000u) && ((cmd[-1].words.w1 & 0x3u) == 0x3u)) { + cmd->words.w0 = fixed.words.w0; + cmd->words.w1 = fixed.words.w1; + } +} + static inline void append_sp_set_geometry_mode_cmd(Gfx** dl, u32 mode) { (*dl)->words.w0 = 0xB7000000u; (*dl)->words.w1 = mode; @@ -346,6 +375,7 @@ RECOMP_PATCH void load_segments(Gfx **arg0, DisplayList *ddl) { if (rc_world_projection_active()) { patch_world_chunk_clip_ratio_dl(); } + patch_tv_screen_static_dl(); gSPSegment((*arg0)++, 0, 0); gSPSegment((*arg0)++, 1, (const void*)(uintptr_t)osVirtualToPhysical(gSegment1Base)); From 087cfb75f4821f8f4b072e16d1ad870d5d072a41 Mon Sep 17 00:00:00 2001 From: BitJager <6281442+filipverlee@users.noreply.github.com> Date: Wed, 21 Oct 2026 11:20:04 +0200 Subject: [PATCH 6/7] Keep the trails on the species terminal screen in 4:3 render_terminal_background_frame cleared the frame to opaque black every frame in phase 1. The original only draws a translucent black rectangle over the previous frame there, so the glyph text, DNA and animal leave fading trails behind them, which is the noise effect on the stats screen. Only clear in widescreen, where the uncleared frame also keeps a native-width copy of the stat text visible. Co-Authored-By: Claude Opus 5.5 --- patches/overlay_context_patches.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/patches/overlay_context_patches.c b/patches/overlay_context_patches.c index c6203c9..ead901b 100644 --- a/patches/overlay_context_patches.c +++ b/patches/overlay_context_patches.c @@ -456,7 +456,12 @@ RECOMP_PATCH void render_terminal_background_frame(void) { gScreenWidth = terminal_frame_width; load_default_display_list(&gMainDL); - patch_clear_overlay_color(&gMainDL); + // The original only darkens the previous frame here (the translucent rectangle above), which + // leaves the trails behind the glyphs, DNA and animal. In widescreen a native-width copy of + // the stat text also stays visible that way, so the frame is only cleared there. + if (terminal_frame_width > SSSV_BASE_WIDTH) { + patch_clear_overlay_color(&gMainDL); + } set_menu_text_color(0xFF, 0xFF, 0xFF, 0xFF); select_font(0, FONT_COMIC_SANS_PATCH, 1, 0); update_terminal_scene_lighting(0xFF); From 059e5a2ef1c362fd4bd3910d1a871147daa7fb02 Mon Sep 17 00:00:00 2001 From: BitJager <6281442+filipverlee@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:36:09 +0200 Subject: [PATCH 7/7] Restore the reflections in the dome glass render_ship_window_projection_replay is how the game draws reflections in the dome glass (e.g. the glass walls in Snow Joke): near a reflecting wall it sets gCameraVisibilityMask[6] and replays the frame's object lists under a mirror matrix multiplied into the projection, before the world geometry, so the mirrored objects show through the translucent glass. The recomp had replaced it with an empty function and removed the matching mirrored visibility checks. Restore both. The translucent object list now starts with a projection reload (the XLU state barrier), which would drop the mirror matrix, so the replay runs that list from the end of the barrier and loads the world modelview itself. Co-Authored-By: Claude Opus 5.5 --- patches/graphics_patches.c | 5 ++ patches/lod_patches.c | 25 +++++++++ patches/ship_window_patches.c | 97 ++++++++++++++++++++++++++++++++++- 3 files changed, 125 insertions(+), 2 deletions(-) diff --git a/patches/graphics_patches.c b/patches/graphics_patches.c index dc4f3ab..d0b9d0d 100644 --- a/patches/graphics_patches.c +++ b/patches/graphics_patches.c @@ -36,6 +36,10 @@ static u32 g_dbg_world_perspective_calls = 0; static u32 g_dbg_depth_clear_calls = 0; static u32 g_dbg_world_depth_failsafe_calls = 0; +// End of the world state at the start of the translucent object list, so the dome reflection replay +// (ship_window_patches.c) can run the list without the projection reload. +Gfx* gXluStateBarrierEnd = NULL; + void setup_intro_perspective_638558(Gfx** dl); static inline void write_main_viewport_expand(s16 viewport_width, s16 viewport_height) { @@ -869,6 +873,7 @@ RECOMP_PATCH void setup_world_perspective_6AB090(DisplayList* arg0) { gSPMatrix(gXluDL++, &arg0->unk374D0, G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); gSPPerspNormalize(gXluDL++, gWorldPerspNorm); #endif + gXluStateBarrierEnd = gXluDL; g_dbg_world_perspective_calls++; if (SHOULD_TRACE_PERIODIC(g_dbg_world_perspective_calls, 6, 240)) { diff --git a/patches/lod_patches.c b/patches/lod_patches.c index 168c85f..3d8dd63 100644 --- a/patches/lod_patches.c +++ b/patches/lod_patches.c @@ -21,6 +21,23 @@ static inline s16 apply_lod_override(s16 visibility) { return visibility; } +// Near a reflecting dome wall (gCameraVisibilityMask[6], see ship_window_patches.c) an object also counts as +// visible when its mirror image is, so its reflection is drawn even while the object itself is off screen. +static inline s32 mirror_image_visible(s32 xPos, s32 zPos, s32 yPos, s32 arg3, s8 arg8, u8 arg9, s32 allow_x_mirror) { + const s32 plane = (gCameraVisibilityMask[6] & 0xFFC) << 0x12; + + if (arg9 != 0) { + return 0; + } + if (gCameraVisibilityMask[6] & 1) { + return classify_visibility_simple(xPos, zPos, plane - yPos, arg3, arg8) == 0; + } + if (allow_x_mirror && (gCameraVisibilityMask[6] & 2)) { + return classify_visibility_simple(plane - xPos, zPos, yPos, arg3, arg8) == 0; + } + return 0; +} + RECOMP_PATCH s16 classify_object_visibility_6FA0A0( s32 xPos, s32 zPos, @@ -44,6 +61,10 @@ RECOMP_PATCH s16 classify_object_visibility_6FA0A0( } } + if (mirror_image_visible(xPos, zPos, yPos, arg3, arg8, arg9, 1)) { + return apply_lod_override(VISIBILITY_VISIBLE); + } + return apply_lod_override(classify_visibility_and_draw_fov_mask(xPos, zPos, yPos, arg3, fovImageIdx, arg5, arg6, arg7, arg8, arg9)); } @@ -70,5 +91,9 @@ RECOMP_PATCH s16 classify_dynamic_visibility_6FA26C( } } + if (mirror_image_visible(xPos, zPos, yPos, arg3, arg8, arg9, 0)) { + return apply_lod_override(VISIBILITY_VISIBLE); + } + return apply_lod_override(classify_visibility_and_draw_fov_mask(xPos, zPos, yPos, arg3, fovImageIdx, arg5, arg6, arg7, arg8, arg9)); } diff --git a/patches/ship_window_patches.c b/patches/ship_window_patches.c index 015533f..d9ff9a6 100644 --- a/patches/ship_window_patches.c +++ b/patches/ship_window_patches.c @@ -1,6 +1,99 @@ #include "sssv_patch_common.h" -// The original ship-window path re-rendered world geometry under a second -// projection. The recomp keeps the normal world pass and drops that replay. +// Reflections in the dome glass (e.g. the glass walls in Snow Joke). When the camera is near a reflecting +// wall, the game sets gCameraVisibilityMask[6] (bit 0: z mirror, bit 1: x mirror, bits 2-11: plane offset) +// and replays the frame's object display lists under a mirror matrix multiplied into the projection. This +// happens before the world geometry is drawn, so the mirrored objects only show through the translucent glass. + +// The patch-side DisplayList type stops before these fields; offsets as in the decomp's DisplayList. +#define DL_OPAQUE_LIST(ctx) ((Gfx*)((u8*)(ctx) + 0x9600)) +#define DL_XLU_LIST(ctx) ((Gfx*)((u8*)(ctx) + 0xBB80)) +#define DL_XLU_LIST_LEN 1000 +#define DL_MIRROR_MTX(ctx) ((Mtx*)((u8*)(ctx) + 0x37510)) +#define DL_AMBIENT(ctx) ((u8*)(ctx) + 0x3B638) // lights.a: col[3], pad, colc[3], pad +#define DL_LIGHT0(ctx) ((u8*)(ctx) + 0x3B640) // lights.l[0]: col[3], pad, colc[3], pad, dir[3], pad +#define DL_MIRROR_AMBIENT(ctx) ((u8*)(ctx) + 0x3B6B0) +#define DL_MIRROR_LIGHT(ctx) ((u8*)(ctx) + 0x3B6B8) + +// F3DEX gSPNumLights(1) and gSPLight(l, n). +static inline void sp_num_lights_1(Gfx* pkt) { + pkt->words.w0 = 0xBC000002u; // G_MOVEWORD, G_MW_NUMLIGHT + pkt->words.w1 = 0x80000040u; // NUML(1) +} + +static inline void sp_light(Gfx* pkt, const void* light, u32 n) { + pkt->words.w0 = _SHIFTL(G_MOVEMEM, 24, 8) | _SHIFTL((n - 1) * 2 + 0x86, 16, 8) | _SHIFTL(16, 0, 16); + pkt->words.w1 = (u32)(uintptr_t)light; +} + +extern void func_80127D30(Mtx* mtx, s16 zOffset); // build_ship_window_z_projection_matrix +extern void func_80127ED4(Mtx* mtx, s16 xOffset); // build_ship_window_x_projection_matrix + +// Start of the translucent object list after the world state that setup_world_perspective_6AB090 puts in +// front of it. That state reloads the projection, which would drop the mirror matrix in the replay. +extern Gfx* gXluStateBarrierEnd; + +static inline void load_world_modelview(void) { + gSPMatrix(gMainDL++, &gDisplayListContext->unk374D0, G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); +} + +static inline void restore_world_projection(void) { + gSPMatrix(gMainDL++, &gDisplayListContext->unk37410, G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_PROJECTION); + gSPMatrix(gMainDL++, &gDisplayListContext->unk37450, G_MTX_NOPUSH | G_MTX_MUL | G_MTX_PROJECTION); + gSPMatrix(gMainDL++, &gDisplayListContext->unk37490, G_MTX_NOPUSH | G_MTX_MUL | G_MTX_PROJECTION); +} + RECOMP_PATCH void render_ship_window_projection_replay(void) { + DisplayList* ctx = gDisplayListContext; + u8* ambient; + u8* light; + u8* mirror_ambient; + u8* mirror_light; + s32 i; + + if (gCameraVisibilityMask[6] & 1) { + func_80127D30(DL_MIRROR_MTX(ctx), (gCameraVisibilityMask[6] & 0xFFC) << 1); + + gSPMatrix(gMainDL++, DL_MIRROR_MTX(ctx), G_MTX_NOPUSH | G_MTX_MUL | G_MTX_PROJECTION); + if ((gXluStateBarrierEnd >= DL_XLU_LIST(ctx)) && (gXluStateBarrierEnd < DL_XLU_LIST(ctx) + DL_XLU_LIST_LEN)) { + load_world_modelview(); + gSPDisplayList(gMainDL++, gXluStateBarrierEnd); + } + load_world_modelview(); + gSPDisplayList(gMainDL++, DL_OPAQUE_LIST(ctx)); + gSPPopMatrix(gMainDL++, G_MTX_MODELVIEW); + + restore_world_projection(); + } else if (gCameraVisibilityMask[6] & 2) { + func_80127ED4(DL_MIRROR_MTX(ctx), (gCameraVisibilityMask[6] & 0xFFC) << 1); + + // The x mirror only replays opaque objects, lit with half the ambient and a quarter of the light. + ambient = DL_AMBIENT(ctx); + light = DL_LIGHT0(ctx); + mirror_ambient = DL_MIRROR_AMBIENT(ctx); + mirror_light = DL_MIRROR_LIGHT(ctx); + for (i = 0; i < 3; i++) { + mirror_ambient[i] = ambient[i] >> 1; + mirror_ambient[4 + i] = ambient[4 + i] >> 1; + mirror_light[i] = light[i] >> 2; + mirror_light[4 + i] = light[i] >> 2; // as in the original: colc from col + mirror_light[8 + i] = light[8 + i]; + } + + sp_num_lights_1(gMainDL++); + sp_light(gMainDL++, mirror_light, 1); + sp_light(gMainDL++, mirror_ambient, 2); + + gSPMatrix(gMainDL++, DL_MIRROR_MTX(ctx), G_MTX_NOPUSH | G_MTX_MUL | G_MTX_PROJECTION); + load_world_modelview(); + gSPDisplayList(gMainDL++, DL_OPAQUE_LIST(ctx)); + + sp_num_lights_1(gMainDL++); + sp_light(gMainDL++, light, 1); + sp_light(gMainDL++, ambient, 2); + + gSPPopMatrix(gMainDL++, G_MTX_MODELVIEW); + + restore_world_projection(); + } }