cmake_minimum_required(VERSION 3.16)
project(Sonic3AndKnucklesRecomp VERSION 0.7.5 LANGUAGES C)
set(S3_BUILD_VERSION "${PROJECT_VERSION}" CACHE STRING "Release version")
if(NOT S3_BUILD_VERSION MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+(-[A-Za-z0-9.]+)?$")
    message(FATAL_ERROR "Invalid S3_BUILD_VERSION")
endif()
add_compile_definitions(S3_BUILD_VERSION="${S3_BUILD_VERSION}")
set(GENESIS_GAME_VERSION "${S3_BUILD_VERSION}")

set(CMAKE_C_STANDARD 11)

# ---------------------------------------------------------------------------
# Sonic 3 & Knuckles â€” native static recompilation.
#
# Sonic 3 and Sonic & Knuckles are two separate ~2 MB ROMs; "Sonic 3 &
# Knuckles" is the 4 MB lock-on UNION of the two (S&K boots at $000000, Sonic 3
# maps at $200000). They share this engine, so one repo hosts all three as
# build modes, one native target each:
#
#   Target            ROM                      Ports (native/oracle)  Status
#   Sonic3Recomp           Sonic 3 alone, 2 MB     4384 / 4385       playable bring-up
#   Sonic3KRecomp          S3+S&K combined, 4 MB   4386 / 4387       scaffold / bring-up
#   SonicAndKnucklesRecomp S&K alone, 2 MB         4388 / 4389       bring-up
#
# Shared framework code (runner + recompiler) comes from RECOMP_ROOT. Game-
# specific adapters, ROM configuration, discovery/annotation inputs, the shared
# family renderer, the pinned skdisasm source and tests belong to this repo:
#   game/common/   sonic3_video.{c,h} + Blue Spheres (all three modes)
#   game/<mode>/   game.toml, discovery files, <mode>_spec.c, owner ROM
#   game/skdisasm/ pinned Sonic Retro source disassembly (submodule)
# See docs/REPOSITORY_OWNERSHIP.md.
# ---------------------------------------------------------------------------

# Engine root. Public layout: the committed `segagenesisrecomp` submodule
# (clone --recursive is self-contained). Local-dev override: if a gitignored
# `engine-local` symlink -> ../segagenesisrecomp is present, prefer it so every
# game repo shares ONE engine checkout. Create it with scripts/link-engine.*.
set(GENESIS_RECOMP_ROOT "" CACHE PATH
    "Optional shared Genesis framework checkout (defaults to engine-local/submodule)")
if(GENESIS_RECOMP_ROOT)
    set(RECOMP_ROOT "${GENESIS_RECOMP_ROOT}")
    message(STATUS "engine: explicit GENESIS_RECOMP_ROOT override")
elseif(EXISTS "${CMAKE_SOURCE_DIR}/engine-local")
    set(RECOMP_ROOT "${CMAKE_SOURCE_DIR}/engine-local")
    message(STATUS "engine: local-dev override (engine-local/ -> shared checkout)")
else()
    set(RECOMP_ROOT "${CMAKE_SOURCE_DIR}/segagenesisrecomp")
    message(STATUS "engine: committed segagenesisrecomp/ submodule")
endif()
set(RUNNER_ROOT "${RECOMP_ROOT}/runner")

option(GENESIS_Z80_RECOMP
    "Run a Genesis sound Z80 through a flat statically-recompiled backend" OFF)
set(GENESIS_Z80_AOT_SOURCE "" CACHE FILEPATH
    "Generated <game>_step.c emitted by SmsRecomp --flat-step")
set(Z80_RECOMP_CORE_ROOT "${RECOMP_ROOT}/external/z80-recomp-core")
if(GENESIS_Z80_RECOMP)
    if(NOT EXISTS "${Z80_RECOMP_CORE_ROOT}/include/sms_runtime.h")
        message(FATAL_ERROR
            "z80-recomp-core submodule is missing; run git submodule update --init --recursive")
    endif()
    if(NOT EXISTS "${GENESIS_Z80_AOT_SOURCE}")
        message(FATAL_ERROR "GENESIS_Z80_AOT_SOURCE does not exist: ${GENESIS_Z80_AOT_SOURCE}")
    endif()
    get_filename_component(GENESIS_Z80_AOT_DIR "${GENESIS_Z80_AOT_SOURCE}" DIRECTORY)
endif()

set(SDL2_BUNDLED "${RUNNER_ROOT}/external/SDL2/SDL2-2.28.5")
# SDL2 discovery. Windows pins the bundled VC devel package (NO_DEFAULT_PATH
# keeps a stray MSYS2 SDL2 from leaking msys64 headers onto cl.exe). macOS/Linux
# use the system/Homebrew SDL2 (Apple Silicon: /opt/homebrew).
if(WIN32)
    set(SDL2_DIR "${SDL2_BUNDLED}/cmake" CACHE PATH "" FORCE)
    find_package(SDL2 REQUIRED CONFIG NO_DEFAULT_PATH PATHS "${SDL2_DIR}")
else()
    if(APPLE)
        find_program(BREW_EXECUTABLE brew)
        if(BREW_EXECUTABLE)
            execute_process(COMMAND "${BREW_EXECUTABLE}" --prefix sdl2
                OUTPUT_VARIABLE _brew_sdl2_prefix
                OUTPUT_STRIP_TRAILING_WHITESPACE
                ERROR_QUIET RESULT_VARIABLE _brew_sdl2_rc)
            if(_brew_sdl2_rc EQUAL 0 AND _brew_sdl2_prefix)
                list(APPEND CMAKE_PREFIX_PATH "${_brew_sdl2_prefix}")
            endif()
        endif()
    endif()
    find_package(SDL2 REQUIRED)
endif()

# The runner includes SDL as <SDL2/SDL.h> (the bundled Windows devel layout).
# Homebrew's SDL2::SDL2 points the include dir *at* .../include/SDL2, so add
# its parent so <SDL2/SDL.h> resolves the same way on every platform.
set(SDL2_PARENT_INCLUDE_DIRS "")
foreach(_inc IN LISTS SDL2_INCLUDE_DIRS)
    get_filename_component(_inc_name "${_inc}" NAME)
    if(_inc_name STREQUAL "SDL2")
        get_filename_component(_inc_parent "${_inc}" DIRECTORY)
        list(APPEND SDL2_PARENT_INCLUDE_DIRS "${_inc_parent}")
    endif()
endforeach()

# ---------------------------------------------------------------------------
# YM2612 FM = vendored ymfm (Aaron Giles, BSD-3) behind the stable ym2612_*
# API (runner/audio/ym2612.h). Replaced clownmdemu's FM (AGPL) 2026-06-05 as
# part of the AGPL-free runtime initiative â€” see segagenesisrecomp/LICENSING.md
# (teardown item #2). enable_language(CXX) because ymfm is C++.
# ---------------------------------------------------------------------------
enable_language(CXX)
set(CMAKE_CXX_STANDARD 17)
set(FM_SOURCES
    "${RUNNER_ROOT}/audio/ym2612_ymfm.cpp"
    "${RUNNER_ROOT}/external/ymfm/src/ymfm_opn.cpp"
    "${RUNNER_ROOT}/external/ymfm/src/ymfm_ssg.cpp"
    "${RUNNER_ROOT}/external/ymfm/src/ymfm_adpcm.cpp"
    "${RUNNER_ROOT}/external/ymfm/src/ymfm_pcm.cpp"
)

# Prod-vs-debug TCP strip (see SonicTheHedgehogRecomp/CMakeLists.txt). ON keeps
# the cmd_server TCP listener + frame_snapshots ring; OFF (release) swaps the
# no-op stub and drops frame_snapshots so the shipped binary opens no port.
option(GEN_ENABLE_TRACE
    "Build the TCP cmd server + observability rings (frame_snapshots). OFF for release." ON)
if(GEN_ENABLE_TRACE)
    set(GEN_TRACE_SOURCES
        "${RUNNER_ROOT}/cmd_server.c"
        "${RUNNER_ROOT}/frame_snapshots.c")
    add_compile_definitions(GEN_ENABLE_TRACE=1)
else()
    set(GEN_TRACE_SOURCES "${RUNNER_ROOT}/cmd_server_stub.c")
endif()

set(RUNNER_SOURCES
    "${RUNNER_ROOT}/main.c"
    "${RUNNER_ROOT}/audio.c"
    ${GEN_TRACE_SOURCES}
    "${RUNNER_ROOT}/glue.c"
    "${RUNNER_ROOT}/fiber_compat.c"   # cross-platform fiber shim (Win32 Fibers / POSIX ucontext) â€” glue.c depends on it
    "${RUNNER_ROOT}/crash_report.c"
    "${RUNNER_ROOT}/input_script.c"
    "${RUNNER_ROOT}/reverse_debug.c"
    "${RUNNER_ROOT}/gamepad.c"
    "${RUNNER_ROOT}/input_map.c"      # per-player Genesis button bindings (kbd+pad, 3/6-btn)
    "${RUNNER_ROOT}/app_config.c"     # settings.ini + rom.cfg load/save
    "${RUNNER_ROOT}/chip_trace.c"
    "${RUNNER_ROOT}/audio/event_queue.c"
    ${FM_SOURCES}
    "${RUNNER_ROOT}/audio/sn76489.c"
    "${RUNNER_ROOT}/audio/mixer.c"
    "${RUNNER_ROOT}/audio/observability.c"
    "${RUNNER_ROOT}/cosim_state.c"  # pointer-free end-state hash for benchmarks + co-sim
    # Verified-enhancement shadow layer (opt-in via env, default OFF; links no
    # clownmdemu, safe to ship). See docs/SHADOW_ENHANCEMENTS.md.
    "${RUNNER_ROOT}/audio/audio_shadow.c"   # envelope-correlation verifier
    "${RUNNER_ROOT}/audio/fm_shadow.cpp"    # parallel ymfm chip (cleaner highs)
    "${RUNNER_ROOT}/video/color_lut.c"      # present-time 9-bit CRT/composite LUT
)

# Clean-room own backend (VDP+bus+Z80 scheduler) â€” the only backend.
set(OWN_BACKEND_SOURCES
    "${RUNNER_ROOT}/video/genesis_vdp.c"
    "${RUNNER_ROOT}/video/genesis_bus.c"
    "${RUNNER_ROOT}/video/genesis_machine.c"
    "${RUNNER_ROOT}/external/superzazu/z80.c")   # MIT Z80 core, embedded by genesis_machine
if(GENESIS_Z80_RECOMP)
    list(APPEND OWN_BACKEND_SOURCES
        "${RUNNER_ROOT}/z80_recomp.c"
        "${GENESIS_Z80_AOT_SOURCE}")
endif()

option(SONIC_REVERSE_DEBUG
    "Tier-1 reverse-debug instrumentation (WRAM store ring; dev builds)" ON)
option(GEN_DEV_TRACE
    "Compile the dev chip/snd trace rings + dumps (NEVER in release builds)" OFF)
option(GENESIS_BUILD_COSIM
    "Build the differential co-simulation targets (dev-only, never shipped)" OFF)

set(COMMON_LIBS SDL2::SDL2main SDL2::SDL2)
if(WIN32)
    list(APPEND COMMON_LIBS ws2_32 comdlg32)
elseif(UNIX)
    list(APPEND COMMON_LIBS m)
endif()

# add_recomp_mode(<target> <prefix> <game_dir> <rom> <native_port> <oracle_port>)
#   Builds <target> (native) for one game mode, generating C in this build tree
#   from this repo's game/<game_dir>/ (an absolute, caller-owned directory).
set(S3_GAME_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/game")
set(S3_GAME_COMMON "${S3_GAME_ROOT}/common")
# Raw machine/host snapshots belong to this source, compiler and build setup.
file(GLOB_RECURSE _state_inputs CONFIGURE_DEPENDS
    "${RUNNER_ROOT}/*.c" "${RUNNER_ROOT}/*.cpp" "${RUNNER_ROOT}/*.h"
    "${S3_GAME_ROOT}/*.c" "${S3_GAME_ROOT}/*.h" "${S3_GAME_ROOT}/*.inc" "${S3_GAME_ROOT}/*.toml"
    "${RECOMP_ROOT}/recompiler/src/*.c" "${RECOMP_ROOT}/recompiler/src/*.h"
    "${RECOMP_ROOT}/external/m68k-recomp-core/*.c" "${RECOMP_ROOT}/external/m68k-recomp-core/*.h")
list(FILTER _state_inputs EXCLUDE REGEX "/(generated|skdisasm|external/SDL2)/")
list(SORT _state_inputs)
set(_state_fingerprint "Sonic3-state-v1;${CMAKE_C_COMPILER_ID};${CMAKE_C_COMPILER_VERSION};${CMAKE_CXX_COMPILER_ID};${CMAKE_CXX_COMPILER_VERSION};${CMAKE_SIZEOF_VOID_P};${CMAKE_SYSTEM_NAME};${CMAKE_SYSTEM_PROCESSOR};${SONIC_REVERSE_DEBUG};${GEN_ENABLE_TRACE};${GEN_DEV_TRACE};${GENESIS_Z80_RECOMP};${CMAKE_C_FLAGS};${CMAKE_CXX_FLAGS}")
foreach(_input IN LISTS _state_inputs)
    set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${_input}")
    file(SHA256 "${_input}" _hash)
    string(APPEND _state_fingerprint ";${_hash}")
endforeach()
if(CMAKE_CONFIGURATION_TYPES)
    set(_state_id "")
    foreach(_config IN LISTS CMAKE_CONFIGURATION_TYPES)
        string(TOUPPER "${_config}" _config_upper)
        string(SHA256 _config_id "${_state_fingerprint};${_config};${CMAKE_C_FLAGS_${_config_upper}};${CMAKE_CXX_FLAGS_${_config_upper}}")
        string(APPEND _state_id "$<$<CONFIG:${_config}>:${_config_id}>")
    endforeach()
else()
    string(TOUPPER "${CMAKE_BUILD_TYPE}" _config_upper)
    string(SHA256 _state_id "${_state_fingerprint};${CMAKE_BUILD_TYPE};${CMAKE_C_FLAGS_${_config_upper}};${CMAKE_CXX_FLAGS_${_config_upper}}")
endif()
file(GENERATE OUTPUT "${CMAKE_BINARY_DIR}/state-$<CONFIG>/sonic3_state_build.h"
    CONTENT "#define SONIC3_STATE_BUILD_ID \"${_state_id}\"\n")
include_directories("${CMAKE_BINARY_DIR}/state-$<CONFIG>")
include("${RECOMP_ROOT}/cmake/GenesisRecompGenerated.cmake")
function(add_recomp_mode TARGET PREFIX GAME_DIR ROM PORT_NATIVE PORT_ORACLE)
    set(GAME "${S3_GAME_ROOT}/${GAME_DIR}")
    genesisrecomp_add_generated_sources(
        GENERATED_SOURCES "${PREFIX}" "${GAME}" "${ROM}"
        REVERSE_DEBUG "${SONIC_REVERSE_DEBUG}")
    set(SHARED_SRC
        ${RUNNER_SOURCES}
        ${GENERATED_SOURCES}
        "${GAME}/${PREFIX}_spec.c"
    )
    list(APPEND SHARED_SRC "${S3_GAME_COMMON}/sonic3_video.c"
        "${S3_GAME_COMMON}/sonic3_gameplay.c")
    # The source's queued decompressor changes its IRQ return PC. Compile this
    # small program with the existing explicit-PC native backend so its RTE
    # bookmark semantics do not depend on a suspended C call continuation.
    find_package(Python3 COMPONENTS Interpreter REQUIRED)
    set(KOS_DIR "${CMAKE_CURRENT_BINARY_DIR}/${PREFIX}-kos-native")
    set(KOS_CORE "${RECOMP_ROOT}/external/m68k-recomp-core/runtime")
    set(KOS_NATIVE "${KOS_DIR}/sonic3_kos_native_module.c")
    foreach(SHARD RANGE 0 31)
        if(SHARD LESS 10)
            set(SHARD_NAME "0${SHARD}")
        else()
            set(SHARD_NAME "${SHARD}")
        endif()
        list(APPEND KOS_NATIVE "${KOS_DIR}/sonic3_kos_native_part${SHARD_NAME}.c")
    endforeach()
    add_custom_command(OUTPUT ${KOS_NATIVE} "${KOS_DIR}/sonic3_kos_native.h"
        COMMAND "${Python3_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/tools/gen_kos_native.py"
            --compiler "$<TARGET_FILE:M68kStepRecomp>" --rom "${GAME}/${ROM}"
            --out "${KOS_DIR}" --mode "${GAME_DIR}"
        DEPENDS M68kStepRecomp "${GAME}/${ROM}" "${CMAKE_SOURCE_DIR}/tools/gen_kos_native.py"
        VERBATIM)
    list(APPEND SHARED_SRC "${S3_GAME_COMMON}/sonic3_kos.c"
        "${KOS_CORE}/m68k_step.c" ${KOS_NATIVE})
    # Game-owned mods that run S&K code: the combined cart and S&K alone.
    if(NOT PREFIX STREQUAL "sonic3")
        list(APPEND SHARED_SRC
            "${S3_GAME_COMMON}/sonic3_state.c"
            "${S3_GAME_COMMON}/sonic3_knuckles_army.c"
            "${S3_GAME_COMMON}/sonic3_knuckles_art.c"
            "${S3_GAME_COMMON}/sonic3_knuckles_title.c"
            "${S3_GAME_COMMON}/sonic3_knuckles_menu.c"
            "${S3_GAME_COMMON}/sonic3_title_art.c"
            "${S3_GAME_COMMON}/sonic3_mods.c")
    endif()
    if(PREFIX STREQUAL "sonic3k")
        file(GLOB STAGE_PACK_SOURCES CONFIGURE_DEPENDS "${S3_GAME_ROOT}/trilogy/*.c")
        # The CD deformation transcription intentionally wraps original
        # fixed-width accumulators. Qualify these
        # semantics on GNU/Clang as well as MSVC; no host assembly is required.
        set_source_files_properties("${S3_GAME_ROOT}/trilogy/scd_background.c"
            PROPERTIES COMPILE_OPTIONS "$<$<C_COMPILER_ID:GNU,Clang,AppleClang>:-fwrapv>")
        list(APPEND SHARED_SRC ${STAGE_PACK_SOURCES})
    endif()
    # Clean-room 68000 interpreter/decoder/rom_parser: the _wt-cosim engine's
    # shared glue.c references m68k_interp_run_framed (Tier-3 native floor)
    # unconditionally, so every target built against it links these (AGPL-free).
    set(FLOOR
        "${RUNNER_ROOT}/m68k_interp.c"
        "${RECOMP_ROOT}/recompiler/src/m68k_decoder.c"
        "${RECOMP_ROOT}/recompiler/src/rom_parser.c")
    # Nothing third-party beyond ymfm/superzazu/clowncommon â€” that is the
    # enforcement that no AGPL header can be compiled into a release binary
    # (clowncommon is ISC-permissive, vendored at runner/external/clowncommon).
    set(INCLUDES_NATIVE
        "${KOS_DIR}" "${KOS_CORE}"
        "${RUNNER_ROOT}" "${RUNNER_ROOT}/include" "${RUNNER_ROOT}/video"
        "${RUNNER_ROOT}/external/ymfm/src"
        "${RUNNER_ROOT}/external/superzazu"
        "${RUNNER_ROOT}/external/clowncommon"
        "${RECOMP_ROOT}/recompiler/src"          # m68k_decoder.h (floor)
        "${GAME}"
        "${S3_GAME_ROOT}/trilogy"
        "${S3_GAME_COMMON}"                      # sonic3_video.h (all modes)
    )
    if(GENESIS_Z80_RECOMP)
        list(APPEND INCLUDES_NATIVE
            "${Z80_RECOMP_CORE_ROOT}/include"
            "${GENESIS_Z80_AOT_DIR}")
    endif()

    # Native â€” recompiled 68K + clean-room own backend (AGPL-free; no clown
    # link, no clown headers).
    add_executable(${TARGET} ${SHARED_SRC} ${OWN_BACKEND_SOURCES} ${FLOOR})
    add_dependencies(${TARGET} ${GENERATED_SOURCES_TARGET})
    target_compile_definitions(${TARGET} PRIVATE
        INCLUDE_GENERATED_CODE=1 ENABLE_RECOMPILED_CODE=1 OWN_BACKEND=1
        DEFAULT_DEBUG_PORT=${PORT_NATIVE}
        $<$<STREQUAL:${PREFIX},sonic3>:SONIC3_STANDALONE=1>
        $<$<BOOL:${GENESIS_Z80_RECOMP}>:GENESIS_Z80_RECOMP=1>
        $<$<BOOL:${GENESIS_Z80_RECOMP}>:SMS_RUNTIME_NO_CALL_BY_ADDRESS=1>
        $<$<BOOL:${SONIC_REVERSE_DEBUG}>:SONIC_REVERSE_DEBUG=1>
        $<$<BOOL:${GEN_DEV_TRACE}>:GEN_DEV_TRACE>)

    target_include_directories(${TARGET} PRIVATE ${INCLUDES_NATIVE})
    foreach(t ${TARGET})
        target_link_libraries(${t} PRIVATE ${COMMON_LIBS})
        if(MSVC)
            target_compile_options(${t} PRIVATE /W3 /WX- /utf-8 /wd4102 /wd4996 /wd4013)
            target_link_options(${t} PRIVATE /MAP)
            set_source_files_properties("${GENERATED_SOURCES_FULL}"
                PROPERTIES COMPILE_FLAGS "/bigobj")
        endif()
        if(WIN32)
            add_custom_command(TARGET ${t} POST_BUILD
                COMMAND ${CMAKE_COMMAND} -E copy_if_different
                    "${SDL2_BUNDLED}/lib/x64/SDL2.dll" "$<TARGET_FILE_DIR:${t}>"
                COMMENT "Copying SDL2.dll for ${t}")
            # Bring-your-own ROM: copy next to the exe only if present. ROMs are
            # gitignored and never committed/distributed (see RELEASING.md).
            if(EXISTS "${GAME}/${ROM}")
                add_custom_command(TARGET ${t} POST_BUILD
                    COMMAND ${CMAKE_COMMAND} -E copy_if_different
                        "${GAME}/${ROM}" "$<TARGET_FILE_DIR:${t}>/${ROM}"
                    COMMENT "Copying ${ROM} for ${t}")
            endif()
        endif()
    endforeach()

    # --- Differential co-sim targets (dev-only; see engine COSIM.md) ---------
    # Mirror native/oracle but add the cosim modules + the clean-room 68000
    # interpreter/decoder/rom_parser (the shared glue.c references them under
    # GENESIS_COSIM; RUNNER_SOURCES omits them). game_cycles.c + the generated
    # <prefix>_cycles.c give the cross-backend work-cycle ruler its cost table.
    # The generation rule emits the cost table with the compiled translations,
    # so a clean build can configure these targets before either file exists.
    if(GENESIS_BUILD_COSIM)
        set(COSIM_FLOOR
            "${RUNNER_ROOT}/m68k_interp.c"
            "${RECOMP_ROOT}/recompiler/src/m68k_decoder.c"
            "${RECOMP_ROOT}/recompiler/src/rom_parser.c")
        add_executable(${TARGET}_cosim ${SHARED_SRC} ${OWN_BACKEND_SOURCES} ${COSIM_FLOOR}
            "${RUNNER_ROOT}/cosim.c"
            "${RUNNER_ROOT}/cosim_visible.c" "${RUNNER_ROOT}/cosim_cycles.c"
            "${RUNNER_ROOT}/game_cycles.c" "${GENERATED_SOURCES_CYCLES}")
        add_dependencies(${TARGET}_cosim ${GENERATED_SOURCES_TARGET})
        target_compile_definitions(${TARGET}_cosim PRIVATE
            INCLUDE_GENERATED_CODE=1 ENABLE_RECOMPILED_CODE=1 OWN_BACKEND=1
            GENESIS_COSIM=1 DEFAULT_DEBUG_PORT=4398
            $<$<STREQUAL:${PREFIX},sonic3>:SONIC3_STANDALONE=1>
            $<$<BOOL:${SONIC_REVERSE_DEBUG}>:SONIC_REVERSE_DEBUG=1>)
        target_include_directories(${TARGET}_cosim PRIVATE ${INCLUDES_NATIVE}
            "${RECOMP_ROOT}/recompiler/src")


        foreach(t ${TARGET}_cosim)
            target_link_libraries(${t} PRIVATE ${COMMON_LIBS})
            if(MSVC)
                target_compile_options(${t} PRIVATE /W3 /WX- /utf-8 /wd4102 /wd4996 /wd4013)
                set_source_files_properties("${GENERATED_SOURCES_FULL}"   PROPERTIES COMPILE_FLAGS "/bigobj")
                set_source_files_properties("${GENERATED_SOURCES_CYCLES}" PROPERTIES COMPILE_FLAGS "/bigobj")
            endif()
            if(WIN32)
                add_custom_command(TARGET ${t} POST_BUILD
                    COMMAND ${CMAKE_COMMAND} -E copy_if_different
                        "${SDL2_BUNDLED}/lib/x64/SDL2.dll" "$<TARGET_FILE_DIR:${t}>"
                    COMMENT "SDL2.dll for ${t}")
                if(EXISTS "${GAME}/${ROM}")
                    add_custom_command(TARGET ${t} POST_BUILD
                        COMMAND ${CMAKE_COMMAND} -E copy_if_different
                            "${GAME}/${ROM}" "$<TARGET_FILE_DIR:${t}>/${ROM}"
                        COMMENT "${ROM} for ${t}")
                endif()
            endif()
        endforeach()
    endif()
endfunction()

# --- Mode: Sonic 3 alone (2 MB @ $000000) â€” playable bring-up ---------------
add_recomp_mode(Sonic3Recomp  sonic3  sonic3  sonic3.bin  4384 4385)

# --- Mode: Sonic 3 & Knuckles combined (4 MB lock-on) â€” scaffold ------------
add_recomp_mode(Sonic3KRecomp sonic3k sonic3k sonic3k.bin 4386 4387)

# --- Mode: Sonic & Knuckles alone (2 MB @ $000000) â€” bring-up ----------------
# Standalone S&K: byte-identical to the low 2 MB of the combined cart, so it
# shares the S&K master boot path and the S&K-half disasm (org 0). Data lives in
# game/sandk/ (game.toml + discovery files regenerated from skdisasm
# sonic3k.lst at offset 0). See game/sandk/game.toml.
add_recomp_mode(SonicAndKnucklesRecomp sandk sandk sandk.bin 4388 4389)

# Matched MSVC runtime qualification retains ordinary compilation for these
# siblings; the S3K target keeps the shared IPO/PGO training workflow.
if(MSVC)
    set_property(TARGET Sonic3Recomp SonicAndKnucklesRecomp
        PROPERTY GENESIS_NATIVE_IPO_DISABLED ON)
endif()

# Record the exact source and submodule inputs for release payload validation.
find_package(Git REQUIRED)
foreach(_repo SOURCE ENGINE UI DISASM)
    if(_repo STREQUAL "SOURCE")
        set(_dir "${CMAKE_SOURCE_DIR}")
    elseif(_repo STREQUAL "ENGINE")
        set(_dir "${RECOMP_ROOT}")
    elseif(_repo STREQUAL "UI")
        set(_dir "${CMAKE_SOURCE_DIR}/recomp-ui")
    else()
        set(_dir "${S3_GAME_ROOT}/skdisasm")
    endif()
    get_filename_component(_dir "${_dir}" REALPATH)
    execute_process(COMMAND "${GIT_EXECUTABLE}" -C "${_dir}" rev-parse HEAD
        OUTPUT_VARIABLE S3_${_repo}_COMMIT OUTPUT_STRIP_TRAILING_WHITESPACE
        RESULT_VARIABLE _git_result)
    if(NOT _git_result EQUAL 0)
        message(FATAL_ERROR "Cannot identify ${_repo} source commit")
    endif()
endforeach()
foreach(_mode sonic3 sonic3k sandk)
    if(_mode STREQUAL "sonic3")
        set(_target Sonic3Recomp)
    elseif(_mode STREQUAL "sonic3k")
        set(_target Sonic3KRecomp)
    else()
        set(_target SonicAndKnucklesRecomp)
    endif()
    configure_file(tools/release/build-info.json.in "release-${_mode}.json" @ONLY)
    file(GENERATE OUTPUT "$<TARGET_FILE_DIR:${_target}>/build-info-${_mode}.json"
        INPUT "${CMAKE_BINARY_DIR}/release-${_mode}.json" TARGET ${_target})
endforeach()

# Shared recomp-ui (Dear ImGui) pre-boot launcher â€” added to ALL THREE native
# targets (each is its own game/exe sharing this repo's output dir). Each stages
# its OWN box art under a distinct name (BOXART_NAME); the runner's seam selects
# it per mode via g_game_spec.boxart -> assets/img/boxart-<mode>.tga. Defines
# RECOMP_LAUNCHER.
set(RECOMP_UI_ENABLE_MODS ON CACHE BOOL "Expose Sonic 3 family's opt-in built-in mods")
include(${CMAKE_SOURCE_DIR}/recomp-ui/recomp_ui.cmake)
recomp_target_launcher_ui(Sonic3Recomp
    CONSOLE genesis
    BOXART ${CMAKE_SOURCE_DIR}/boxart-sonic3.tga  BOXART_NAME boxart-sonic3.tga)
recomp_target_launcher_ui(Sonic3KRecomp
    CONSOLE genesis
    BOXART ${CMAKE_SOURCE_DIR}/boxart-sonic3k.tga BOXART_NAME boxart-sonic3k.tga)
recomp_target_launcher_ui(SonicAndKnucklesRecomp
    CONSOLE genesis
    BOXART ${CMAKE_SOURCE_DIR}/boxart-sonick.tga  BOXART_NAME boxart-sonick.tga)

# Public owned-disc conversion also needs its bounded graph validator when
# tests/debugging are disabled in a release build.
add_executable(scd_pack_validate tools/scd_pack_validate.c
    game/trilogy/scd_resource.c game/trilogy/scd_pack.c)
target_include_directories(scd_pack_validate PRIVATE "${CMAKE_SOURCE_DIR}/game/trilogy")
set_source_files_properties("${CMAKE_SOURCE_DIR}/game/trilogy/scd_import.c" PROPERTIES
    COMPILE_DEFINITIONS "S3_CD_IMPORT_SOURCE=\"${CMAKE_SOURCE_DIR}/tools/import_sonic_cd.py\";S3_ROM_IMPORT_SOURCE=\"${CMAKE_SOURCE_DIR}/tools/import_campaign.py\";S3_CD_IMPORT_VALIDATOR=\"$<TARGET_FILE:scd_pack_validate>\"")

# Optional developer target. Release packagers always freeze/stage this helper;
# ordinary native builds do not require the PyInstaller/Pillow build packages.
find_package(Python3 REQUIRED COMPONENTS Interpreter)
add_custom_target(s3_campaign_builder
    COMMAND "${Python3_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/tools/make_campaign_builder.py"
        --out "$<TARGET_FILE_DIR:Sonic3KRecomp>"
        --work "${CMAKE_BINARY_DIR}/campaign-builder" --engine-root "${RECOMP_ROOT}"
    VERBATIM)

option(BUILD_TESTING "Build ROM-independent renderer and framework tests" ON)
if(BUILD_TESTING)
    enable_testing()
    add_subdirectory("${RECOMP_ROOT}/tests" "${CMAKE_BINARY_DIR}/engine-tests")
    add_subdirectory(tests)
endif()
