Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16)
project(SuperMetroidSNESRecomp VERSION 0.3.15 LANGUAGES C)
project(SuperMetroidSNESRecomp VERSION 0.3.16 LANGUAGES C)

# Cross-platform (macOS/Linux/MinGW) build for SuperMetroidSNESRecomp.
#
Expand All @@ -11,6 +11,8 @@ project(SuperMetroidSNESRecomp VERSION 0.3.15 LANGUAGES C)
# - SDL2 + OpenGL

set(CMAKE_C_STANDARD 11)
option(SM_VIEW_ACTIVITY "Process enemies throughout the visible wider view" ON)
add_compile_definitions(SM_VIEW_ACTIVITY=$<BOOL:${SM_VIEW_ACTIVITY}>)
option(SM_RASTER_FULL_COPY "Use full-copy VRAM raster storage as the reference" OFF)
option(SM_RASTER_TRACK_WRITES "Track VRAM writes for title-owned capture intervals" ON)
add_compile_definitions(SM_RASTER_TRACK_WRITES=$<BOOL:${SM_RASTER_TRACK_WRITES}>)
Expand Down Expand Up @@ -58,6 +60,9 @@ list(APPEND SNESRECOMP_RUNNER_INCLUDE_DIRS ${SNESRECOMP_ROOT}/runner/src/desktop
# roots through the owning helper, or directly for focused tests/tools that
# compile only that layer.
if(BUILD_TESTING AND NOT ANDROID)
add_executable(sm_activity_test tests/sm_activity_test.c src/sm_activity.c)
target_include_directories(sm_activity_test PRIVATE src)
add_test(NAME sm_visible_enemy_activity COMMAND sm_activity_test)
add_subdirectory(${SNESRECOMP_ROOT}/runner/tests/raster_snapshot raster-snapshot-tests)
add_executable(sm_display_test tests/sm_display_test.c src/sm_display.c)
target_include_directories(sm_display_test PRIVATE
Expand Down Expand Up @@ -129,6 +134,25 @@ if(BUILD_TESTING AND NOT ANDROID)
add_test(NAME sm_custom_video
COMMAND sm_video_test ${CMAKE_CURRENT_BINARY_DIR}/sm-video-test.ini)

# The pause menu's Options tab: guest pause-state effects and pixels. It
# saves sm-video.ini, so it runs in its own directory, never beside the
# game executable.
add_executable(sm_pause_options_test
tests/sm_pause_options_test.c src/sm_pause_options.c src/sm_video.c
${SNESRECOMP_ROOT}/runner/src/desktop/host_clock.c
${SNESRECOMP_ROOT}/runner/src/snes_overlay_draw.c
${RECOMP_UI_ROOT}/src/common/recomp_runtime_ui.c)
target_include_directories(sm_pause_options_test PRIVATE src ${RECOMP_UI_ROOT}/src
${SNESRECOMP_RUNNER_INCLUDE_DIRS}
${SNESRECOMP_ROOT}/runner/src/desktop)
# sm_pause_options.c includes host_api.h, which includes the SDL boundary
# header; the test fakes the API so no SDL runs, but the includes and
# compile definitions still need the selected backend.
snesrecomp_target_sdl(sm_pause_options_test)
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/sm_pause_options_test.d)
add_test(NAME sm_pause_options COMMAND sm_pause_options_test
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/sm_pause_options_test.d)

add_executable(sm_renderer_test tests/sm_renderer_test.c src/sm_renderer.c src/sm_mode7.c)
target_include_directories(sm_renderer_test PRIVATE
src ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
Expand Down Expand Up @@ -162,7 +186,7 @@ if(BUILD_TESTING AND NOT ANDROID)
# Assertions are the test oracle, including Release configurations.
foreach(sm_test sm_display_test sm_overlay_blit_test
sm_savestate_gesture_test ppu_window_test sm_video_test
sm_renderer_test sm_mode7_test)
sm_pause_options_test sm_renderer_test sm_mode7_test)
if(MSVC)
target_compile_options(${sm_test} PRIVATE /UNDEBUG)
else()
Expand All @@ -171,6 +195,7 @@ if(BUILD_TESTING AND NOT ANDROID)
endforeach()
if(NOT WIN32)
target_link_libraries(sm_video_test PRIVATE m)
target_link_libraries(sm_pause_options_test PRIVATE m)
target_link_libraries(sm_renderer_test PRIVATE m)
target_link_libraries(sm_render_capture PRIVATE m)
target_link_libraries(sm_mode7_test PRIVATE m)
Expand Down Expand Up @@ -212,6 +237,8 @@ set(SM_HOST_SOURCES
src/sm_mode7.c # SM's mode-7 object projection
src/sm_display.c # SM presentation geometry
src/sm_video.c # SM aspect/HUD policy + presentation clock
src/sm_activity.c src/sm_activity_host.c
src/sm_pause_options.c # 3rd pause tab: the Game Options overlay
src/sm_mods.c # built-in presentation mod provider
)

Expand Down Expand Up @@ -273,7 +300,7 @@ file(GLOB SM_REPLAY_ID_INPUTS CONFIGURE_DEPENDS src/*.c src/*.h recomp/*.cfg pro
list(APPEND SM_REPLAY_ID_INPUTS ${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.txt
${CMAKE_CURRENT_SOURCE_DIR}/framework_pins.txt ${CMAKE_CURRENT_SOURCE_DIR}/VERSION)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${SM_REPLAY_ID_INPUTS})
set(SM_REPLAY_SOURCE_ID "${CMAKE_C_FLAGS};${CMAKE_C_FLAGS_RELEASE};${CMAKE_CXX_FLAGS};${CMAKE_CXX_FLAGS_RELEASE}")
set(SM_REPLAY_SOURCE_ID "${CMAKE_C_FLAGS};${CMAKE_C_FLAGS_RELEASE};${CMAKE_CXX_FLAGS};${CMAKE_CXX_FLAGS_RELEASE};activity=${SM_VIEW_ACTIVITY}")
foreach(input IN LISTS SM_REPLAY_ID_INPUTS)
file(SHA256 ${input} input_hash)
string(APPEND SM_REPLAY_SOURCE_ID ";${input_hash}")
Expand Down Expand Up @@ -369,6 +396,9 @@ endif()
recomp_target_launcher_ui(SuperMetroidSNESRecomp
CONSOLE snes
BOXART ${CMAKE_CURRENT_SOURCE_DIR}/recomp/launcher/boxart.tga)
if(SNESRECOMP_ENABLE_NET)
snesrecomp_enable_recomp_net(SuperMetroidSNESRecomp)
endif()

# Omit the codegen host AND its include directory. The shared desktop host
# detects that capability by header availability; the shipped launcher only
Expand Down
41 changes: 41 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -1097,6 +1097,47 @@ time is the PLM loop (force_lle) and analyzer cascades in HDMA objects and
enemy AI (beads-8wg.2.148). The PLP-with-unknown-P exit-width hazard is
beads-8wg.2.147.

## 2026-10-20: pause Options tab edits the runner's display settings

The pause panel's Options tab previously reached only the five Super Metroid
video settings. It now also offers the five settings the runner already
applies live: fullscreen (windowed/borderless), vsync (off/on/adaptive),
frame blend, run-ahead (0..4), and window scale (1..10, disabled while
fullscreen, where the host ignores it) — plus a graceful Exit. Eleven rows
total, plus the existing Back-to-map action.

The values cross the game/host boundary through the framework's new
`host_api.h` get/set pairs, and persist through the host marking the config
dirty and writing `config.ini` at shutdown; no second writer on the game
side. Window scale re-enables automatically when the user leaves
fullscreen, and navigation skips the disabled row in both directions,
matching the panel's existing disabled-row semantics. The Exit row asks
the host to end the run (`snesrecomp_request_exit`): the main loop exits
on its next pass, after the requesting frame has presented, through the
normal shutdown path — so a config the panel changed is written before
the window closes.

The pin for this is **open framework work**: snesrecomp
[PR186](https://github.com/RetroPortingToolKit/snesrecomp/pull/186) (head
`adf6f6d09765570ad89c7f1bf523b02509e0ab8f` over `f68aa0f`, the previous
pin) adds the API, three new keymap entries, and the dirty-config shutdown
write; the submodule is pinned one commit beyond the PR head,
`425b53214b5e03c9f5405fc667e83b50911f9c21` (local branch
`pr186-with-request-exit`), which adds `snesrecomp_request_exit`. That
commit exists only locally until it is pushed to the PR's branch, and the
pin follows the PR to merge and must be re-verified when it lands. The PR's
new keys only act if bound in `[KeyMap]`; the shipped config binds none, so
existing keybinds are unaffected (the key enum shifted, but saved binds
store names).

The panel test (`tests/sm_pause_options_test.c`) fakes the API with the
host's own clamps and drives all eleven rows: choice wrap at both ends, INT
clamping, the fullscreen-dependent enable row, the Exit action's host
latch, and the existing pad ownership, fade-out, snapshot, and gesture
sections. Its golden render hashes cover the eleven-row layout at
256/342/456/684 px; regenerate with `PAUSE_OPTIONS_TEST_CALIBRATE` if the
layout or rows change. All 10 test targets pass.

## Owner-gated (do NOT do without explicit decision)

Merging `investigate/sm-0012-blocker` or the multi-tier branches to main;
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.3.15
0.3.16
10 changes: 10 additions & 0 deletions docs/custom-renderer.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ This is an experimental custom renderer. The requested worktree and Mods
functionality is implemented; this is not a full-game visual-fidelity or
sustained-performance certification.

## Simulation in the wider view

The renderer remains read-only. A separate simulation viewport extends the
ROM's enemy processing queues in slot order and the common player/enemy
projectile culls. Native 4:3 retains the original behavior. Enemies can attack
sooner when visible in a wider view. Fit samples its window geometry before
simulation; replay records that geometry, and snapshots preserve it. Netplay
uses the host's fixed initial viewport for every peer throughout the match.
The portable ROM-only activity reference remains a build-time selection.

## October 25 feedback corrections

HUD/dialogue BG3 pages remain screen-space through message HDMA and boss
Expand Down
11 changes: 11 additions & 0 deletions docs/releases/v0.3.16.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
This update fixes widescreen artifacts reported during the latest playthrough and adds the in-game Options tab.

- Item and dialogue messages preserve HUD placement. Power bombs no longer bleed into the HUD, and the moving HUD pixel strip and unstable edge bars are corrected.
- Widescreen backgrounds, door transitions and liquid effects use the correct room and raster data. Crocomire's body stays anchored to the room when it leaves the original screen boundary.
- Enemies remain active across the wider visible view. They can notice and attack Samus sooner than in 4:3. Shared projectile bounds follow that view too.
- The Options tab applies settings during play, survives current-build save/load, and exits through normal configuration and native-save persistence.
- Run-ahead audio output, long-frame SPC catch-up, pause transitions and current-build snapshot validation are corrected.

The owner accepted the prepared Windows visual/gameplay checks. Steam Deck hardware was unavailable for the remaining ordinary-gameplay audio check; that report remains unverified on the device.

Keep your native SRAM saves and settings when updating. Emulator save states belong to their exact build. Supply your own supported ROM; downloads contain the precompiled runtime and assets only.
8 changes: 4 additions & 4 deletions framework_pins.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
snesrecomp=afe5656530708da22e644ece76e3c96b06b626c2
recomp-ui=cac2b8f4aaa5d1061c6b8f2c3ed7e73c46b0400e
recomp-net=8c76fb643339144a3a84d5ba8f5defe6250dcd26
retcomm-rbengine=2a03e73693acee0fb78076ea058642c931bef12e
snesrecomp=246c0d40a8bc5f706bc6ef71d7e8633842f8e42e
recomp-ui=ef13e3f1a00e2e03f91e4b74343fd77213f8a234
recomp-net=8c76fb643339144a3a84d5ba8f5defe6250dcd26
retcomm-rbengine=2a03e73693acee0fb78076ea058642c931bef12e
2 changes: 1 addition & 1 deletion recomp-ui
1 change: 1 addition & 0 deletions recomp/bank02.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ func CopyToVramAtNextInterrupt e039 end:e071
func LoadRoomMusic e071 end:e09b
func UpdateMusicTrackIndex e09b end:e0d5
hle_func e039 HleSmDoorTransitionVramWait
hle_func 90e8 HleSmGameState_15_Paused_Async
func LoadNewMusicTrackIfChanged e0d5 end:e113
func NullFunc e113 end:e118
func PlayRoomMusicTrackAfterAFrames e118 end:e139
Expand Down
3 changes: 3 additions & 0 deletions recomp/bank06.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

bank = 06

hle_func e6e0 HleSmCheckIfEprojIsOffScreen
func SmNativeEprojCullBody e6e2 end:e722

# >>> AUTO-INGESTED FROM sm DECOMP — do not hand-edit between markers >>>
# Source: snesrev/sm decomp PC comments.
# Regenerate via: python tools/ingest_sm_decomp.py
Expand Down
3 changes: 3 additions & 0 deletions recomp/bank10.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

bank = 10

hle_func b16a HleSmDeleteProjectileIfFarOffScreen
func SmNativeProjectileCullBody b16d end:b197

# >>> AUTO-INGESTED FROM sm DECOMP — do not hand-edit between markers >>>
# Source: snesrev/sm decomp PC comments.
# Regenerate via: python tools/ingest_sm_decomp.py
Expand Down
4 changes: 4 additions & 0 deletions recomp/bank20.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

bank = 20

# Keep original queue construction and CPU residue; add visible margin slots.
hle_func 8eb6 HleSmDetermineWhichEnemiesToProcess
func SmNativeEnemyQueueBody 8eb7 end:8fd4


# Dynamic JML sites still using the prototype host dispatcher. Migrate these
# individually once their live continuations and width variants are covered.
Expand Down
2 changes: 1 addition & 1 deletion snesrecomp
119 changes: 119 additions & 0 deletions src/gen_stubs.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
#include <stdlib.h>
#include "cpu_state.h"
#include "common_rtl.h"
#include "common_cpu_infra.h"
#include "cpu_trace.h"
#include "variables.h"
#include "sm_rtl.h"
#include "sm_pause_options.h"
#include "sm_activity.h"

/* Wait replacements unwind compiled calls to the interruptible scheduler.
* The original ROM bytes own the WRAM handshake and return-frame pop; the
Expand All @@ -28,6 +31,104 @@ RecompReturn HleSmWaitForNmi(CpuState *cpu) {
return sm_scheduler_yield(cpu, ((uint32)cpu->PB << 16) | 0x8338u);
}

/* $A0:8EB6 ends in PLB/RTL. It has no nested calls or waits: the bounded
* interpreter call completes the original queue and architectural residue
* before we add visible margin enemies. No guest property bits are edited. */
static void sm_prologue_cycles(CpuState *cpu, unsigned cycles) {
cpu->coprocessor_master_cycles = cpu->master_cycles;
cpu->cycles += cycles;
cpu->master_cycles += cycles * ((g_memsel && (cpu->PB & 0x80)) ? 6 : 8);
}
static void sm_push16(CpuState *cpu, uint16 value) {
cpu_write16(cpu, 0, (uint16)(cpu->S - 1), value); cpu->S -= 2;
}
static RecompReturn sm_native_tail(CpuState *cpu, uint32 body, uint32 entry,
uint16 entry_s, uint8 hrv, unsigned frame) {
cpu_tailcall_inherit_return_context(entry_s, hrv);
return cpu_dispatch_pc_from(cpu, body, (uint16)(entry_s + frame), entry);
}
RecompReturn HleSmDetermineWhichEnemiesToProcess(CpuState *cpu) {
uint32 entry = ((uint32)cpu->PB << 16) | 0x8eb6u;
uint32 body = (entry & 0xff0000u) | 0x8eb7u;
if (!SmActivityExtra() && !cpu->m_flag && !cpu->x_flag && cpu_dispatch_has_entry(cpu, body)) {
if (interp_bridge_lle_master_deadline_reached(cpu))
return interp_bridge_lle_yield_unwind(cpu, entry);
uint16 entry_s = cpu->S; uint8 hrv = cpu->host_return_valid;
/* PHB; the generated tail owns PHK/PLB and the entire original queue. */
sm_prologue_cycles(cpu, 3);
cpu_write8(cpu, 0, cpu->S, cpu->DB); --cpu->S;
return sm_native_tail(cpu, body, entry, entry_s, hrv, 3);
}
RecompReturn result = interp_tier_run_call_frame(cpu, entry, entry, 3, NULL);
#if SM_VIEW_ACTIVITY
if (result == RECOMP_RETURN_NORMAL && !SmActivityAugmentQueues(g_ram, SmActivityExtra())) {
static bool warned;
if (!warned) fprintf(stderr, "[sm-activity] invalid/full enemy queue; retained original ROM queues\n");
warned = true;
}
#endif
return result;
}

/* Widen the horizontal predicate only. The ROM still owns the vertical
* test, deletion effects, return carry/value and register restoration. */
static bool sm_margin_projectile(int16 x, int left, int right) {
#if SM_VIEW_ACTIVITY
int extra = (int)SmActivityExtra();
return extra && (x < left || x >= right) && x >= left - extra && x < right + extra;
#else
return false;
#endif
}
RecompReturn HleSmDeleteProjectileIfFarOffScreen(CpuState *cpu) {
uint32 entry = ((uint32)cpu->PB << 16) | 0xb16au;
if (!cpu->m_flag && !cpu->x_flag && !cpu->_flag_D) {
uint16 index = cpu_read16(cpu, cpu->DB, 0x0dde);
int16 x = (int16)(cpu_read16(cpu, cpu->DB, (uint16)(0x0b64 + index)) -
cpu_read16(cpu, cpu->DB, 0x0911));
if (index < 20 && !(index & 1) && sm_margin_projectile(x, -64, 320)) {
cpu->X = index; /* Original LDX $0DDE; Y path overwrites A/N/Z/C/V. */
sm_prologue_cycles(cpu, 5);
return interp_tier_run_call_frame(cpu, (entry & 0xff0000u) | 0xb184u, entry, 2, NULL);
}
}
uint32 body = (entry & 0xff0000u) | 0xb16du;
if (!cpu->m_flag && !cpu->x_flag && cpu_dispatch_has_entry(cpu, body)) {
if (interp_bridge_lle_master_deadline_reached(cpu))
return interp_bridge_lle_yield_unwind(cpu, entry);
uint16 entry_s = cpu->S; uint8 hrv = cpu->host_return_valid;
sm_prologue_cycles(cpu, 5); /* LDX $0DDE */
cpu->X = cpu_read16(cpu, cpu->DB, 0x0dde);
cpu->_flag_Z = cpu->X == 0; cpu->_flag_N = (cpu->X >> 15) != 0;
cpu->P = (cpu->P & ~0x82) | (cpu->_flag_Z ? 2 : 0) | (cpu->_flag_N ? 0x80 : 0);
return sm_native_tail(cpu, body, entry, entry_s, hrv, 2);
}
return interp_tier_run_call_frame(cpu, entry, entry, 2, NULL);
}
RecompReturn HleSmCheckIfEprojIsOffScreen(CpuState *cpu) {
uint32 entry = ((uint32)cpu->PB << 16) | 0xe6e0u;
if (!cpu->m_flag && !cpu->x_flag && !cpu->_flag_D && cpu->X < 36 && !(cpu->X & 1)) {
int16 x = (int16)(cpu_read16(cpu, cpu->DB, (uint16)(0x1a4b + cpu->X)) -
cpu_read16(cpu, cpu->DB, 0x0911));
if (sm_margin_projectile(x, 0, 256)) {
uint16 entry_s = cpu->S;
/* PHX/PHY from $E6E0, before the original Y predicate at $E6FC.
* Pass the caller watermark, so PLY/PLX do not end the bridge early. */
sm_prologue_cycles(cpu, 8); sm_push16(cpu, cpu->X); sm_push16(cpu, cpu->Y);
return interp_tier_dispatch_balanced(cpu, (entry & 0xff0000u) | 0xe6fcu, entry, entry_s, 2, false);
}
}
uint32 body = (entry & 0xff0000u) | 0xe6e2u;
if (!cpu->m_flag && !cpu->x_flag && cpu_dispatch_has_entry(cpu, body)) {
if (interp_bridge_lle_master_deadline_reached(cpu))
return interp_bridge_lle_yield_unwind(cpu, entry);
uint16 entry_s = cpu->S; uint8 hrv = cpu->host_return_valid;
sm_prologue_cycles(cpu, 8); sm_push16(cpu, cpu->X); sm_push16(cpu, cpu->Y);
return sm_native_tail(cpu, body, entry, entry_s, hrv, 2);
}
return interp_tier_run_call_frame(cpu, entry, entry, 2, NULL);
}

/* $82:E039: inline-parameter VRAM update coroutine. */
RecompReturn HleSmDoorTransitionVramWait(CpuState *cpu) {
return sm_scheduler_yield(cpu, 0x82E039u);
Expand Down Expand Up @@ -217,3 +318,21 @@ SM_JML1784_WRAPPER(HleSmJmlIndirect1784_A3AC, 0x20A3ACu)

#undef SM_JML1784_WRAPPER
#undef SM_JML178C_WRAPPER

/* GameState_15_Paused_Async ($82:90E8), the pause screen's per-frame state.
* Reached through Game_RunOneFrameOfGame's JSR ($8981,X); ends in RTS.
*
* The Options pause tab (src/sm_pause_options.c) reads and edits this
* frame's pad and pause-menu state first; then the ROM's own routine runs,
* unmodified, in both execution modes. Under the LLE scheduler that is a
* yield back to the interpreter at the routine's entry. Otherwise the ROM
* bytes run in the interpreter tier, which consumes the caller's 2-byte
* frame and still bounces nested calls into compiled code
* (docs/HLE_FUNC.md, "Delegating to the original"). */
RecompReturn HleSmGameState_15_Paused_Async(CpuState *cpu) {
const uint32 entry = ((uint32)cpu->PB << 16) | 0x90E8u;
SmPauseOptions_GuestTick();
if (interp_bridge_in_lle_scheduler())
return interp_bridge_lle_yield_unwind(cpu, entry);
return interp_tier_run_call_frame(cpu, entry, entry, 2, NULL);
}
Loading
Loading