cmake_minimum_required(VERSION 3.16)
project(SuperMetroidSNESRecomp VERSION 0.4.2 LANGUAGES C)

# Cross-platform (macOS/Linux/MinGW) build for SuperMetroidSNESRecomp.
#
# Mirrors the shared framework source list (snesrecomp/runner/runner.cmake)
# plus the Super Metroid-specific runtime in src/. Prerequisites:
#   - a snesrecomp checkout at ./snesrecomp (junction/symlink to the sibling repo)
#   - generated C in src/gen/ and recomp/funcs.h:
#       ./tools/regen.sh --strict-idempotent
#   - 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)
# Qualified Windows x64 default; retain explicit selections and other-target floors.
set(_SM_RASTER_IMPL_DEFAULT "LLE")
string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _SM_RASTER_IMPL_PROCESSOR)
if(WIN32 AND CMAKE_SIZEOF_VOID_P EQUAL 8 AND
   _SM_RASTER_IMPL_PROCESSOR MATCHES "^(amd64|x86_64|x64)$")
    set(_SM_RASTER_IMPL_DEFAULT "NATIVE")
endif()
set(SM_RASTER_IMPL "${_SM_RASTER_IMPL_DEFAULT}" CACHE STRING "Captured raster implementation (LLE or NATIVE)")
set_property(CACHE SM_RASTER_IMPL PROPERTY STRINGS LLE NATIVE)
if(SM_RASTER_IMPL STREQUAL "NATIVE")
    set(SM_RENDERER_SOURCE src/sm_renderer_native.c)
    set_property(SOURCE src/main.c APPEND PROPERTY COMPILE_DEFINITIONS SM_NATIVE_RASTER=1)
elseif(SM_RASTER_IMPL STREQUAL "LLE")
    set(SM_RENDERER_SOURCE src/sm_renderer.c)
else()
    message(FATAL_ERROR "SM_RASTER_IMPL must be LLE or NATIVE")
endif()
option(SM_NATIVE_ROW_PLAN "Plan full-view native Mode-1 composition per row" OFF)
option(SM_NATIVE_CENTER_REUSE "Reuse qualified native-center pixels for discrete presentations" OFF)
if(SM_NATIVE_CENTER_REUSE AND (NOT SM_NATIVE_ROW_PLAN OR NOT SM_RASTER_IMPL STREQUAL "NATIVE"))
    message(FATAL_ERROR "SM_NATIVE_CENTER_REUSE requires NATIVE raster and SM_NATIVE_ROW_PLAN")
endif()
set_property(SOURCE src/sm_renderer_native.c src/main.c APPEND PROPERTY COMPILE_DEFINITIONS
    SM_NATIVE_ROW_PLAN=$<BOOL:${SM_NATIVE_ROW_PLAN}>
    SM_NATIVE_CENTER_REUSE=$<BOOL:${SM_NATIVE_CENTER_REUSE}>)
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}>)
set_property(SOURCE src/sm_renderer.c APPEND PROPERTY COMPILE_DEFINITIONS
    SM_RASTER_FULL_COPY=$<BOOL:${SM_RASTER_FULL_COPY}>)

# CACHE PATH, so -DSNESRECOMP_ROOT=<engine checkout> works -- the framework
# README documents that flag as the way to build a game against an engine
# worktree. A plain set() here created a normal variable that SHADOWED the
# cache entry, so the override was accepted and then silently ignored: the
# build linked this repo's pinned snesrecomp against C emitted by a DIFFERENT
# engine, which surfaces as undefined runner symbols at link time rather than
# as a clear "wrong engine" error. SMW and MMX already use this form.
set(SNESRECOMP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/snesrecomp" CACHE PATH
    "Path to the snesrecomp checkout used by this game")

find_package(Python3 COMPONENTS Interpreter REQUIRED)

if(NOT ANDROID)
    include(CTest)
endif()
# Android uses the SDL2 source tree fetched by android/fetch_sdl.sh. Desktop
# keeps the framework default (currently SDL3).
if(ANDROID)
    set(SNESRECOMP_SDL_BACKEND "SDL2" CACHE STRING
        "SDL backend (SDL3 or SDL2)" FORCE)
endif()

# Gameplay and QoL features ship as packages for the engine's mod runtime
# (snesrecomp docs/MOD_PACKAGES.md); widescreen and presentation FPS stay
# built-in features (src/sm_mods.c) on the same Mods page.
set(SNESRECOMP_ENABLE_MODS ON CACHE BOOL "Shared mod package runtime" FORCE)
include(${SNESRECOMP_ROOT}/runner/runner.cmake)

# Releases contain the complete native program. Missing generated code must
# never silently substitute a first-run compiler wizard for a playable game.
if(SNESRECOMP_SETUP_HOST)
    message(FATAL_ERROR "Super Metroid requires precompiled generated code; setup-host builds are retired. Run tools/regen.sh with your ROM first.")
endif()

# The desktop host's headers (host_clock.h, ...) moved out of the runner's
# default include set; every target here that uses them needs the directory.
list(APPEND SNESRECOMP_RUNNER_INCLUDE_DIRS ${SNESRECOMP_ROOT}/runner/src/desktop)
# After runner.cmake, so these targets can use ${SNESRECOMP_RUNNER_INCLUDE_DIRS}
# rather than naming runner/src subdirectories themselves -- the runner's
# sources live in layer folders (cpu/, debug/, desktop/, ...) and that
# variable is the framework's contract for finding their headers.
# for the base runner and SNES headers. Optional layers add their own include
# 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_global_controls_test tests/sm_global_controls_test.c)
    target_include_directories(sm_global_controls_test PRIVATE src/mods ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    add_test(NAME sm_global_controls COMMAND sm_global_controls_test)
    add_executable(sm_spin_animation_test tests/sm_spin_animation_test.c)
    target_include_directories(sm_spin_animation_test PRIVATE src/mods ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    add_test(NAME sm_spin_animation COMMAND sm_spin_animation_test)
    add_executable(sm_difficulty_test tests/sm_difficulty_test.c)
    target_include_directories(sm_difficulty_test PRIVATE src/mods ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    add_test(NAME sm_difficulty COMMAND sm_difficulty_test)
    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
        src
        ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    add_test(NAME sm_display_geometry COMMAND sm_display_test)

    # The overlay compositing step (framework helper, this port's only
    # consumer of it so far). See the test's header for why it is worth one.
    add_executable(sm_overlay_blit_test
        tests/sm_overlay_blit_test.c
        ${SNESRECOMP_ROOT}/runner/src/snes_overlay_draw.c)
    target_include_directories(sm_overlay_blit_test PRIVATE
        ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    add_test(NAME sm_overlay_blit COMMAND sm_overlay_blit_test)

    # The open-gesture guard. A host that skips it gets a browser that
    # reopens the frame after it closes, forever -- a frozen game.
    add_executable(sm_savestate_gesture_test
        tests/sm_savestate_gesture_test.c
        ${SNESRECOMP_ROOT}/runner/src/snes_savestate_menu.c
        ${SNESRECOMP_ROOT}/runner/src/snes_overlay_draw.c)
    target_include_directories(sm_savestate_gesture_test PRIVATE
        ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    # Use the selected desktop backend on every release runner. Looking for
    # SDL2 headers breaks a toolchain which only supplies the default SDL3.
    snesrecomp_target_sdl(sm_savestate_gesture_test)
    add_test(NAME sm_savestate_gesture COMMAND sm_savestate_gesture_test)

    add_executable(ppu_window_test tests/ppu_window_test.c)
    target_include_directories(ppu_window_test PRIVATE
        ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    add_test(NAME ppu_widescreen_windows COMMAND ppu_window_test)
    add_test(NAME sm_door_audio_trace
        COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/door_audio_trace_test.py)
    add_test(NAME sm_release_payload
        COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/release_payload_test.py)
    add_test(NAME sm_coverage_campaign
        COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/coverage_campaign_test.py)
    set_tests_properties(sm_coverage_campaign PROPERTIES
        ENVIRONMENT "PYTHONPATH=${SNESRECOMP_ROOT}/recompiler")
    add_test(NAME sm_replay_inspector
        COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/replay_diagnostic_test.py)
    add_subdirectory(${SNESRECOMP_ROOT}/runner/tests/diagnostics diagnostic-writer-tests)

endif()


# Launcher UI backend: the SHARED recomp-ui launcher (F:\Projects\recomp-ui,
# consumed as the ./recomp-ui junction/submodule) so every recomp game drives
# ONE launcher repo. The former in-tree launcher_ng has been retired from the
# engine in favour of this shared, console-agnostic launcher.
# CACHE PATH for the same reason as SNESRECOMP_ROOT: a debug worktree, or a
# build pinned against a specific launcher revision, needs to point this
# somewhere other than the in-tree submodule.
set(RECOMP_UI_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/recomp-ui" CACHE PATH
    "Path to the shared recomp-ui checkout used by this game")
include(${RECOMP_UI_ROOT}/recomp_ui.cmake)

if(BUILD_TESTING AND NOT ANDROID)
    add_executable(sm_video_test
        tests/sm_video_test.c src/sm_video.c src/sm_mods.c
        ${SNESRECOMP_ROOT}/runner/src/desktop/host_clock.c)
    target_include_directories(sm_video_test PRIVATE src ${RECOMP_UI_ROOT}/src
        ${SNESRECOMP_RUNNER_INCLUDE_DIRS}
        ${SNESRECOMP_ROOT}/runner/src/desktop)
    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 src/sm_rom_glyphs.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)
    # Calibration mode prints the actual golden hashes instead of asserting,
    # so the constants in the test can be regenerated after a layout change.
    if (PAUSE_OPTIONS_TEST_CALIBRATE)
        target_compile_definitions(sm_pause_options_test PRIVATE
            PAUSE_OPTIONS_TEST_CALIBRATE=1)
    endif()
    # 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})
    add_test(NAME sm_custom_renderer
        COMMAND sm_renderer_test ${CMAKE_CURRENT_BINARY_DIR}/sm-renderer-test.capture)
    add_executable(sm_renderer_native_test tests/sm_renderer_test.c
        tests/sm_renderer_native_variant.c src/sm_mode7.c tests/sm_renderer_ppu_stub.c)
    target_include_directories(sm_renderer_native_test PRIVATE
        src ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    add_test(NAME sm_native_renderer
        COMMAND sm_renderer_native_test ${CMAKE_CURRENT_BINARY_DIR}/sm-renderer-native-test.capture)
    target_compile_definitions(sm_renderer_native_test PRIVATE
        SM_RASTER_FULL_COPY=$<BOOL:${SM_RASTER_FULL_COPY}>
        SM_NATIVE_CENTER_REUSE=1)
    target_compile_definitions(sm_renderer_test PRIVATE
        SM_RASTER_FULL_COPY=$<BOOL:${SM_RASTER_FULL_COPY}>)
    # Compile the new path explicitly, independent of the product default.
    add_executable(sm_row_plan_test tests/sm_row_plan_test.c src/sm_mode7.c tests/sm_renderer_ppu_stub.c)
    target_include_directories(sm_row_plan_test PRIVATE src ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    if(MSVC)
        target_compile_options(sm_row_plan_test PRIVATE /UNDEBUG)
    else()
        target_compile_options(sm_row_plan_test PRIVATE -UNDEBUG)
    endif()
    if(NOT WIN32)
        target_link_libraries(sm_row_plan_test PRIVATE m)
    endif()
    add_test(NAME sm_full_view_row_plan COMMAND sm_row_plan_test)
    add_executable(sm_render_capture_native tools/sm_render_capture.c
        src/sm_renderer_native.c src/sm_video.c src/sm_mode7.c
        ${SNESRECOMP_ROOT}/runner/src/desktop/host_clock.c tests/sm_renderer_ppu_stub.c)
    target_include_directories(sm_render_capture_native PRIVATE src
        ${SNESRECOMP_RUNNER_INCLUDE_DIRS} ${SNESRECOMP_ROOT}/runner/src/desktop)
    if(NOT WIN32)
        target_link_libraries(sm_render_capture_native PRIVATE m)
    endif()
    add_executable(sm_renderer_fullcopy_test tests/sm_renderer_test.c
        tests/sm_renderer_variant.c src/sm_mode7.c)
    target_include_directories(sm_renderer_fullcopy_test PRIVATE
        src ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    target_compile_definitions(sm_renderer_fullcopy_test PRIVATE SM_RASTER_FULL_COPY=1)
    if(MSVC)
        target_compile_options(sm_renderer_fullcopy_test PRIVATE /UNDEBUG)
    else()
        target_compile_options(sm_renderer_fullcopy_test PRIVATE -UNDEBUG)
    endif()
    if(NOT WIN32)
        target_link_libraries(sm_renderer_fullcopy_test PRIVATE m)
    endif()
    add_test(NAME sm_custom_renderer_fullcopy COMMAND sm_renderer_fullcopy_test
        ${CMAKE_CURRENT_BINARY_DIR}/sm-renderer-fullcopy-test.capture)
    add_executable(sm_mode7_test tests/sm_mode7_test.c src/sm_mode7.c)
    target_include_directories(sm_mode7_test PRIVATE src)
    add_test(NAME sm_custom_mode7 COMMAND sm_mode7_test)
    # super-metroid.controls input transforms against a synthetic WRAM.
    add_executable(sm_controls_test tests/sm_controls_test.c
        ${SNESRECOMP_ROOT}/runner/src/snes_overlay_draw.c)
    target_include_directories(sm_controls_test PRIVATE src/mods
        ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    add_test(NAME sm_controls COMMAND sm_controls_test)

    add_executable(sm_low_health_alarm_test tests/sm_low_health_alarm_test.c)
    target_include_directories(sm_low_health_alarm_test PRIVATE src/mods
        ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    add_test(NAME sm_low_health_alarm COMMAND sm_low_health_alarm_test)

    add_executable(sm_single_missile_doors_test tests/sm_single_missile_doors_test.c)
    target_include_directories(sm_single_missile_doors_test PRIVATE src/mods
        ${SNESRECOMP_ROOT}/runner/src)
    add_test(NAME sm_single_missile_doors COMMAND sm_single_missile_doors_test)
    add_executable(sm_laser_test tests/sm_laser_test.c)
    target_include_directories(sm_laser_test PRIVATE src src/mods
        "${SNESRECOMP_ROOT}/runner/src" "${SNESRECOMP_ROOT}/runner/src/snes")
    add_test(NAME sm_laser_sight COMMAND sm_laser_test)
    add_executable(sm_qol_data_test tests/sm_qol_data_test.c
        src/mods/sm_screen_shake_plugin.c src/mods/sm_station_plugins.c
        src/mods/sm_cinematic_skip_plugin.c)
    target_include_directories(sm_qol_data_test PRIVATE src/mods
        ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    add_test(NAME sm_qol_data_consumers COMMAND sm_qol_data_test)
    add_executable(sm_balance_test tests/sm_balance_test.c src/mods/sm_balance_plugins.c)
    target_include_directories(sm_balance_test PRIVATE src/mods
        ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    add_test(NAME sm_tourian_corpse_palette COMMAND sm_balance_test)
    add_executable(sm_bugfix_test tests/sm_bugfix_test.c src/mods/sm_bugfix_plugins.c)
    target_include_directories(sm_bugfix_test PRIVATE src/mods
        ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    add_test(NAME sm_vanilla_reserve_fixes COMMAND sm_bugfix_test)
    add_executable(sm_world_dump tools/sm_world_dump.c src/mods/sm_world.c)
    target_include_directories(sm_world_dump PRIVATE src/mods)
    add_executable(sm_world_test tests/sm_world_test.c src/mods/sm_world.c)
    # Owned-cartridge probes take explicit ROM/WRAM arguments; they are not
    # synthetic CTest passes or release qualification by themselves.
    add_executable(sm_room_preview_test tests/sm_room_preview_test.c
        src/sm_room_preview.c src/mods/sm_world.c)
    target_include_directories(sm_room_preview_test PRIVATE src)
    add_executable(sm_room_runtime_test tests/sm_room_runtime_test.c
        src/sm_room_runtime.c src/sm_activity.c src/mods/sm_world.c
        ${SNESRECOMP_ROOT}/runner/src/snes/interp816.c)
    target_include_directories(sm_room_runtime_test PRIVATE src ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    add_executable(sm_room_doors_test tests/sm_room_doors_test.c
        src/sm_room_runtime.c src/sm_activity.c src/mods/sm_world.c
        ${SNESRECOMP_ROOT}/runner/src/snes/interp816.c)
    target_include_directories(sm_room_doors_test PRIVATE src ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    add_executable(sm_rooms_test tests/sm_rooms_test.c src/sm_rooms.c src/sm_room_preview.c
        src/sm_room_runtime.c src/sm_room_collision.c src/sm_activity.c src/mods/sm_world.c
        ${SNESRECOMP_ROOT}/runner/src/snes/interp816.c)
    target_include_directories(sm_rooms_test PRIVATE src ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    foreach(target sm_room_preview_test sm_room_runtime_test sm_room_doors_test sm_rooms_test)
        if(MSVC)
            target_compile_options(${target} PRIVATE /UNDEBUG)
        else()
            target_compile_options(${target} PRIVATE -UNDEBUG -Wall -Wextra)
        endif()
    endforeach()
    target_include_directories(sm_world_test PRIVATE src/mods)
    add_test(NAME sm_world_model COMMAND sm_world_test)
    add_executable(sm_map_test tests/sm_map_test.c src/mods/sm_world.c
        src/mods/sm_map_view.c src/mods/sm_map_art.c
        ${SNESRECOMP_ROOT}/runner/src/sha256.c ${SNESRECOMP_ROOT}/runner/src/snes_overlay_draw.c)
    target_include_directories(sm_map_test PRIVATE src/mods ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    add_test(NAME sm_map_views COMMAND sm_map_test)
    add_executable(sm_quit_test tests/sm_quit_test.c src/mods/sm_return_to_title_plugin.c)
    target_include_directories(sm_quit_test PRIVATE src/mods ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    add_test(NAME sm_quit_file_select COMMAND sm_quit_test)
    add_executable(sm_movement_test tests/sm_movement_test.c src/mods/sm_movement_plugins.c
        src/mods/sm_controls_plugin.c ${SNESRECOMP_ROOT}/runner/src/snes_overlay_draw.c)
    target_include_directories(sm_movement_test PRIVATE src/mods ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    add_test(NAME sm_movement_extras COMMAND sm_movement_test)
    add_executable(sm_render_capture tools/sm_render_capture.c src/sm_renderer.c src/sm_video.c src/sm_mode7.c
        ${SNESRECOMP_ROOT}/runner/src/desktop/host_clock.c)
    target_include_directories(sm_render_capture PRIVATE src
        ${SNESRECOMP_RUNNER_INCLUDE_DIRS}
        ${SNESRECOMP_ROOT}/runner/src/desktop)
    # 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_pause_options_test sm_renderer_test sm_renderer_native_test sm_mode7_test
              sm_controls_test sm_global_controls_test sm_spin_animation_test sm_difficulty_test sm_low_health_alarm_test sm_single_missile_doors_test sm_laser_test sm_balance_test sm_qol_data_test sm_bugfix_test sm_world_test sm_map_test sm_quit_test sm_movement_test)
        if(MSVC)
            target_compile_options(${sm_test} PRIVATE /UNDEBUG)
        else()
            target_compile_options(${sm_test} PRIVATE -UNDEBUG)
        endif()
    endforeach()
    foreach(sm_hd sm_renderer_test sm_renderer_native_test sm_row_plan_test
            sm_renderer_fullcopy_test sm_render_capture sm_render_capture_native)
        target_sources(${sm_hd} PRIVATE ${SNESRECOMP_ROOT}/runner/src/snes/hd_jobs.c)
    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_renderer_native_test PRIVATE m)
        target_link_libraries(sm_render_capture PRIVATE m)
        target_link_libraries(sm_mode7_test PRIVATE m)
    endif()
endif()

# Generated recompiler output (produced by tools/regen.sh; not committed).
#
if(BUILD_TESTING AND NOT ANDROID)
    foreach(target sm_renderer_test sm_renderer_native_test sm_row_plan_test
                   sm_render_capture_native sm_renderer_fullcopy_test sm_render_capture)
        target_sources(${target} PRIVATE src/sm_room_preview.c src/mods/sm_world.c)
    endforeach()
endif()

# SM_GEN_DIR is overridable so a build can be pointed at generated C emitted by
# a DIFFERENT engine revision without disturbing this repo's src/gen. That is
# what an engine A/B needs: same sources, same flags, two emits. SMK and F-Zero
# already expose the same knob.
set(SM_GEN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/gen" CACHE PATH
    "Directory containing the private generated Super Metroid C sources")
# Added by snesrecomp_target_generated_code() below. Generation is a
# developer/release step; missing output is a hard build error.

# What this port actually owns.
#
# Everything that is not Super Metroid now comes from the framework through
# the snesrecomp_target_* helpers below. This list used to also carry private
# forks of config.c, post_mortem.c, glsl_shader.c, opengl.c and
# fiber_compat.c — roughly 2,400 lines of host code that was either identical
# to the framework's or ahead of it, and which therefore stopped inheriting
# fixes in both directions. The parts this port was ahead on (the Android
# fiber backend, the post-mortem's DB-tripwire and stack/dispatch dumps, the
# GL presenter itself) were moved UP into snesrecomp rather than deleted.
set(SM_HOST_SOURCES
    ${SNESRECOMP_RUNNER_SOURCES}
    # game-specific runtime
    src/main.c            # host shim: identity + this title's hooks
    src/sm_startup.c      # session log, including launcher and early failures
    src/sm_replay.c       # default bounded automatic diagnostic replay
    src/sm_rtl.c          # THE PORT: single-fiber frame model, NMI/IRQ, HDMA
    src/sm_cpu_infra.c    # RtlGameInfo registration
    src/sm_post_mortem.c  # the "sm" crash-report section (framework hook)
    src/sm_spc_player.c   # SPC upload interception
    ${SM_RENDERER_SOURCE} # read-only custom widescreen renderer
    src/sm_room_preview.c # independent connected-room scenery
    src/sm_room_runtime.c src/sm_rooms.c src/sm_seamless.c
    src/sm_room_collision.c src/sm_room_collision_host.c
    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_rom_glyphs.c    # M2 ROM glyph rasterizer (no asset extraction)
    src/sm_mods.c         # built-in presentation mod provider
    # Gameplay/QoL mod plugins, selected by mods/preloaded packages
    src/mods/sm_mod_support.c
    src/mods/sm_mod_overlay.c
    src/mods/sm_elevator_plugin.c
    src/mods/sm_shinespark_drain_plugin.c
    src/mods/sm_xray_plugin.c
    src/mods/sm_station_plugins.c
    src/mods/sm_controls_plugin.c
    src/mods/sm_global_controls_plugin.c
    src/mods/sm_spin_animation_plugin.c
    src/mods/sm_difficulty_plugins.c
    src/mods/sm_hud_plugins.c
    src/mods/sm_low_health_alarm_plugin.c
    src/mods/sm_laser_sight_plugin.c
    src/mods/sm_reveal_map_plugin.c
    src/mods/sm_cinematic_skip_plugin.c
    src/mods/sm_door_plugin.c
    src/mods/sm_single_missile_doors_plugin.c
    src/mods/sm_item_pickup_plugin.c
    src/mods/sm_screen_shake_plugin.c
    src/mods/sm_return_to_title_plugin.c
    src/mods/sm_movement_plugins.c
    src/mods/sm_heavy_physics_plugin.c
    src/mods/sm_combat_plugins.c
    src/mods/sm_balance_plugins.c
    src/mods/sm_bugfix_plugins.c
    src/mods/sm_world.c
    src/mods/sm_map_view.c
    src/mods/sm_map_art.c
    src/mods/sm_map_plugin.c
)

if(ANDROID)
    # SDLActivity loads "libmain.so" and calls its SDL_main.
    add_library(SuperMetroidSNESRecomp SHARED ${SM_HOST_SOURCES})
    set_target_properties(SuperMetroidSNESRecomp PROPERTIES OUTPUT_NAME main)
else()
    add_executable(SuperMetroidSNESRecomp ${SM_HOST_SOURCES})
    if(WIN32)
        set_target_properties(SuperMetroidSNESRecomp PROPERTIES WIN32_EXECUTABLE TRUE)
    endif()
endif()


# NOT the repo root. The root holds VERSION, and on a case-insensitive
# filesystem (Windows, and macOS by default) an -I of the root makes libc++'s
# `#include <version>` resolve to that file instead of the standard header --
# the compiler then reads "0.1.0" as C++ and the build dies inside <cmath>.
# Nothing here includes anything root-relative; keep it off the search path.
# snesrecomp_target_desktop_host() arms the framework's
# snesrecomp_guard_header_shadowing(), which refuses to configure if any
# include directory grows such a file again -- on Linux too, so it is caught
# here rather than in Windows CI.
target_include_directories(SuperMetroidSNESRecomp PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/src
    ${CMAKE_CURRENT_SOURCE_DIR}/recomp
    ${SNESRECOMP_RUNNER_INCLUDE_DIRS}
)

# Desktop SDL is selected through the shared framework helper. Android already
# has the vendored SDL2 target from android/app/jni/CMakeLists.txt, so wire that
# target directly instead of asking find_package() for a host installation.
if(ANDROID)
    target_link_libraries(SuperMetroidSNESRecomp PRIVATE SDL2::SDL2)
    target_compile_definitions(SuperMetroidSNESRecomp PRIVATE
        SNESRECOMP_SDL3=0
        SDL_MAIN_HANDLED)
else()
    snesrecomp_target_sdl(SuperMetroidSNESRecomp)
endif()

# ── Shared host services, from the framework instead of a private fork ──────
#
# The desktop host itself: launcher flow, ROM resolution, config.ini +
# [KeyMap] (which is what gives the in-game overlays their hotkeys), the
# window and the SDL/GL presenters, audio, gamepads, the save-state browser
# and rewind filmstrip, the pacing clock, crash handlers and the post-mortem
# report (TIER2 adds the interpreter-gap manifest, which this port reads after
# every run). src/main.c is the shim that names this title and supplies its
# hooks. Before 2026-09 all of it was a 2,800-line private copy of the host in
# src/main.c, ahead of the framework's in places and behind it in others.
snesrecomp_target_desktop_host(SuperMetroidSNESRecomp TIER2)
find_package(Git REQUIRED)
execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
    OUTPUT_VARIABLE SM_REPLAY_REVISION OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY)
file(GLOB SM_REPLAY_ID_INPUTS CONFIGURE_DEPENDS
    src/*.c src/*.h src/*.inc src/mods/*.c src/mods/*.h recomp/*.cfg profiles/*.json)
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};activity=${SM_VIEW_ACTIVITY};raster=${SM_RASTER_IMPL};row-plan=${SM_NATIVE_ROW_PLAN};center-reuse=${SM_NATIVE_CENTER_REUSE};apu-time=${SNESRECOMP_APU_TIME_IMPL};crc=${SNESRECOMP_CRC32_IMPL}")
foreach(input IN LISTS SM_REPLAY_ID_INPUTS)
    file(SHA256 ${input} input_hash)
    string(APPEND SM_REPLAY_SOURCE_ID ";${input_hash}")
endforeach()
string(SHA256 SM_REPLAY_SOURCE_ID "${SM_REPLAY_SOURCE_ID}")
string(SUBSTRING ${SM_REPLAY_SOURCE_ID} 0 16 SM_REPLAY_SOURCE_ID)
string(APPEND SM_REPLAY_REVISION "-${SM_REPLAY_SOURCE_ID}")
execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
    WORKING_DIRECTORY ${SNESRECOMP_ROOT}
    OUTPUT_VARIABLE SM_REPLAY_ENGINE OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY)
set_property(SOURCE src/sm_replay.c APPEND PROPERTY COMPILE_DEFINITIONS
    SM_REPLAY_REVISION="${SM_REPLAY_REVISION}" SM_REPLAY_ENGINE="${SM_REPLAY_ENGINE}"
    SM_REPLAY_TARGET="${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}-${CMAKE_C_COMPILER_ID}-${CMAKE_C_COMPILER_VERSION}-${CMAKE_BUILD_TYPE}")
string(SHA256 SM_SNAPSHOT_ID "${SM_REPLAY_SOURCE_ID};${SM_REPLAY_ENGINE};${CMAKE_SYSTEM_NAME};${CMAKE_SYSTEM_PROCESSOR};${CMAKE_C_COMPILER_ID};${CMAKE_C_COMPILER_VERSION};${CMAKE_BUILD_TYPE};${SNESRECOMP_ENABLE_TRACE}")
string(SUBSTRING ${SM_SNAPSHOT_ID} 0 24 SM_SNAPSHOT_ID)
set_property(SOURCE src/sm_rtl.c APPEND PROPERTY COMPILE_DEFINITIONS
    SM_SNAPSHOT_ID="sm2-${SM_SNAPSHOT_ID}")
# Every platform compiles and links the same generated program.
snesrecomp_target_generated_code(SuperMetroidSNESRecomp ${SM_GEN_DIR})
# gen_stubs.c holds this port's hand-written bodies for hle_func targets. They
# are part of the recompiled code's surface -- they call into generated banks
# and are called from them -- so they belong to the same build as src/gen and
# not to a setup host, which has neither.
target_sources(SuperMetroidSNESRecomp PRIVATE src/gen_stubs.c)
# rom.cfg at the repo root (git-ignored, the ROM's absolute path) is staged
# beside the executable when none is there yet, so a launch opens on it.
snesrecomp_target_rom_cache(SuperMetroidSNESRecomp)
# The tracked config.ini seeds the one beside the executable when none is
# there yet; after that the file is the player's.
snesrecomp_target_config_seed(SuperMetroidSNESRecomp)
snesrecomp_target_mod_catalog(SuperMetroidSNESRecomp
    "${CMAKE_CURRENT_SOURCE_DIR}/mods/preloaded")

# ROM identity. rom_identity.txt is the one place the digests live: this turns
# it into snesrecomp_rom_identity.h for src/main.c, and tools/regen.sh,
# scripts/package_release.sh and the release workflow read the same file.
# Before this call the digests were written out THREE times — here in
# rom_identity.txt, as a byte array plus a CRC literal in src/main.c, and
# again in src/codegen_setup.c, whose comment claimed to be the single source
# of truth. A revision bump is one edit now.
snesrecomp_rom_identity(SuperMetroidSNESRecomp ${CMAKE_CURRENT_SOURCE_DIR}/rom_identity.txt)

# Win32 Fiber API on ucontext (desktop) / pthreads (Android) — sm_rtl.c's
# single-fiber frame model runs on it. The Android backend in the framework
# copy came from this port.

# Release version stamp: configure with -DSNESRECOMP_BUILD_VERSION=<ver>
# (the CMake equivalent of the MSVC games' /p:SnesRecompBuildVersion); it
# lands in the post-mortem report's "build" section via the
# SNESRECOMP_BUILD_VERSION define. Local builds default to PROJECT_VERSION;
# explicitly setting the cache entry empty keeps main.c's "dev" fallback.
set(SNESRECOMP_BUILD_VERSION "${PROJECT_VERSION}" CACHE STRING
    "Release version stamp baked into crash reports (empty = dev)")
if(SNESRECOMP_BUILD_VERSION)
    set_property(SOURCE src/main.c src/sm_replay.c APPEND PROPERTY COMPILE_DEFINITIONS
        SNESRECOMP_BUILD_VERSION="${SNESRECOMP_BUILD_VERSION}")
endif()

target_compile_definitions(SuperMetroidSNESRecomp PRIVATE
    SMW_DISABLE_LM=1
    SYSTEM_VOLUME_MIXER_AVAILABLE=0   # Win32 WASAPI volume control excluded
    SNESRECOMP_TRACE=$<BOOL:${SNESRECOMP_ENABLE_TRACE}>
    SNESRECOMP_REVERSE_DEBUG=0        # raw WRAM stores; matches non-debug generated C
)

# The recompiled C is machine-generated; quiet its warnings and tolerate the
# K&R-style implicit declarations clang/gcc reject by default.
if(NOT MSVC)
    target_compile_options(SuperMetroidSNESRecomp PRIVATE
        -w -Wno-implicit-function-declaration)
endif()

if(ANDROID)
    target_link_libraries(SuperMetroidSNESRecomp PRIVATE
        ${SNESRECOMP_RUNNER_LIBRARIES}
        android
        log
    )
else()
    target_link_libraries(SuperMetroidSNESRecomp PRIVATE
        ${SNESRECOMP_RUNNER_LIBRARIES}
    )
endif()

# The post-mortem's Win32 DbgHelp link (StackWalk64, SymFromAddr) and the GL
# presenter's OpenGL::GL link both come with snesrecomp_target_desktop_host()
# above, which knows it needs them.

# Dear ImGui pre-boot launcher (GUI) from the shared recomp-ui repo. One call
# wires sources/includes/RECOMP_LAUNCHER define/CXX/asset-staging (same SM
# boxart; recomp-ui's default pad.tga is already the SNES pad).
# CONSOLE is required by recomp-ui: it selects exactly one validated
# assets/consoles/<id>/ manifest, so an SNES game cannot silently stage N64
# cartridge or NES pad art next to the exe.
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
# selects/verifies the player's ROM and starts the precompiled game.

# GLSL shader presets (CRT/scanlines) for the OpenGL desktop backend. The
# launcher's shader dropdown scans assets/shaders beside the executable for
# *.glsl/*.glslp; src/main.c opts this game into that row.
if(NOT ANDROID)
    snesrecomp_target_stage_dir(SuperMetroidSNESRecomp
        ${CMAKE_CURRENT_SOURCE_DIR}/assets/shaders
        assets/shaders)
endif()
