cmake_minimum_required(VERSION 3.25)
project(MaxSpin LANGUAGES C CXX)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
# A newer build machine can link against the oldest supported host's libm.
# Its SONAME remains libm.so.6; no extra library is shipped or loaded at runtime.
set(TOPSPIN_MATH_LIBRARY m CACHE STRING "Math library matching the supported host ABI")
get_filename_component(ROOT "${CMAKE_CURRENT_SOURCE_DIR}/.." ABSOLUTE)
set(TOPSPIN_SDK_DEFAULT "${ROOT}/vendor/xboxrecomp")
set(TOPSPIN_SDK_ROOT "${TOPSPIN_SDK_DEFAULT}" CACHE PATH "Current C runtime SDK")
set(SDK "${TOPSPIN_SDK_ROOT}")
if(NOT WIN32)
    set(SDL3_DIR "${CMAKE_CURRENT_SOURCE_DIR}/cmake/sdl3-host" CACHE PATH "Host SDL3")
endif()
set(XBOXRECOMP_SDL3 ON CACHE BOOL "" FORCE)
set(XBOXRECOMP_VULKAN ON CACHE BOOL "" FORCE)
add_subdirectory(${SDK} runtime)
if(NOT Vulkan_FOUND OR NOT SHADERC_FOUND)
    # The SDK discovers these in a subdirectory; require the actual Vulkan target.
    get_target_property(BACKEND_DEFS xbox_d3d8 INTERFACE_COMPILE_DEFINITIONS)
    if(NOT "XBOXRECOMP_HAVE_VULKAN=1" IN_LIST BACKEND_DEFS)
        message(FATAL_ERROR "This probe requires the Vulkan backend")
    endif()
endif()
find_package(SDL3 REQUIRED CONFIG)
find_package(Vulkan REQUIRED)
# Persistent parent owns its X11 window; no SDK/guest/Vulkan initialization.
if(NOT WIN32)
add_executable(topspin_window_host src/window_host.c src/window_host_sdl.c src/graphics_settings.c src/graphics_settings_posix.c)
target_include_directories(topspin_window_host PRIVATE src)
target_link_libraries(topspin_window_host PRIVATE SDL3::SDL3 ${CMAKE_DL_LIBS})
endif()
set(TOPSPIN_HANDOFF_SOURCE src/image_handoff.c)
set(TOPSPIN_SETTINGS_IO_SOURCE src/graphics_settings_posix.c)
set(TOPSPIN_CONTROLLER_SESSION_SOURCE src/controller_family_session.c)
set(TOPSPIN_SAVE_TRANSACTION_SOURCE src/material_save_transaction.c)
set(TOPSPIN_SCHEDULER_SOURCE src/scheduler_lateness_trace.c)
if(WIN32)
    set(TOPSPIN_SCHEDULER_SOURCE "${ROOT}/windows/native/src/scheduler_lateness_windows.c")
    set(TOPSPIN_SAVE_TRANSACTION_SOURCE)
    set(TOPSPIN_HANDOFF_SOURCE "${ROOT}/windows/native/src/image_handoff_windows.c")
    set(TOPSPIN_SETTINGS_IO_SOURCE "${ROOT}/windows/native/src/graphics_settings_windows.c")
    set(TOPSPIN_CONTROLLER_SESSION_SOURCE "${ROOT}/windows/native/src/controller_family_session_windows.c")
endif()
option(TOPSPIN_RUNTIME_ONLY "Build runtime libraries while the XBE pipeline runs" OFF)
if(TOPSPIN_RUNTIME_ONLY)
    return()
endif()
set(TOPSPIN_IMAGE game CACHE STRING "Independent XBE image for this probe")
include("${CMAKE_CURRENT_SOURCE_DIR}/config/images.cmake")
set_property(CACHE TOPSPIN_IMAGE PROPERTY STRINGS ${TOPSPIN_IMAGE_NAMES})
# Multiple images share one compiled runtime and retain distinct title objects.
# The primary target remains compatible with the existing developer commands.
set(TOPSPIN_ADDITIONAL_IMAGES "" CACHE STRING "Additional whitelisted images to build")
function(topspin_add_image image target_name)
    set(TOPSPIN_IMAGE "${image}")
    include("${CMAKE_CURRENT_SOURCE_DIR}/config/images.cmake")
    file(GLOB GENERATED CONFIGURE_DEPENDS "src/recomp/${image}/*.c")
    if(NOT GENERATED)
        message(FATAL_ERROR "Run pipeline.py ${image} before configuring")
    endif()
    if(WIN32)
        # A token checkpoint at every lifted block lets original spinning loops
        # yield safely. Keep derived code in the build tree, never edit inputs.
        set(WINDOWS_GENERATED)
        foreach(source IN LISTS GENERATED)
            get_filename_component(leaf "${source}" NAME)
            file(READ "${source}" lifted)
            string(REGEX REPLACE "\n(loc_[0-9A-Fa-f]+:)[ \t]*;" "\n\\1 ;guest_turn_checkpoint();" lifted "${lifted}")
            set(derived "${CMAKE_CURRENT_BINARY_DIR}/generated/${image}/${leaf}")
            file(CONFIGURE OUTPUT "${derived}" CONTENT "#include \"guest_turn_windows.h\"\n${lifted}" @ONLY)
            list(APPEND WINDOWS_GENERATED "${derived}")
        endforeach()
        set(GENERATED ${WINDOWS_GENERATED})
    endif()
    add_executable(${target_name} src/main.c src/host_shutdown.c src/foreign_window.c src/controls.c src/controller_actions.c src/cpu_probe.c src/frame_capture.c src/black_frame_watch.c src/render_config.c src/hardware/ac97_bm.c src/hardware/nv2a_regs.c ${GENERATED})
    target_sources(${target_name} PRIVATE src/timing_trace.c src/present_timing_csv.c ${TOPSPIN_SCHEDULER_SOURCE} src/physics_trace.c src/render_trace.c ${TOPSPIN_HANDOFF_SOURCE} src/career_trace.c
        src/graphics_settings.c ${TOPSPIN_SETTINGS_IO_SOURCE} src/sprite_ui_admission.c src/sprite_ui_runtime.c src/sprite_ui_graphics.c)
    if(image STREQUAL "game")
        target_sources(${target_name} PRIVATE src/game_loading_hooks.c src/loading_layout.c)
        target_link_options(${target_name} PRIVATE "-Wl,--wrap=sub_00138340" "-Wl,--wrap=sub_001AE760" "-Wl,--wrap=sub_001B4C30")
        target_sources(${target_name} PRIVATE src/fence_guest_hooks.c)
        target_sources(${target_name} PRIVATE src/timing_hooks.c src/video_hooks.c src/physics_hooks.c src/render_hooks.c src/replay_sample_capture.c src/replay_sample_hooks.c src/game_rumble_hooks.c)
        target_compile_definitions(${target_name} PRIVATE TOPSPIN_REPLAY_SAMPLER_OBSERVER=1 TOPSPIN_SCENE_LIFECYCLE_OBSERVER=1)
        target_sources(${target_name} PRIVATE src/scene_lifecycle.c src/game_scene_lifecycle_hooks.c src/scene_gpu_bridge.c src/scene_render_runtime.c src/scene_actor_frames.c src/actor_runtime.c src/actor_binding_capture.c src/actor_draw_binding.c src/actor_draw_hooks.c src/actor_journal_binding.c src/actor_pb_adapter.c src/constant_lineage_runtime.c src/constant_word_lineage.c src/visual_source_archive.c src/scene_output_frames.c src/visual_pb_guest_hooks.c src/actor_binding_cpu_guard.c src/visual_output_clock.c)
        target_link_options(${target_name} PRIVATE "-Wl,--wrap=sub_0018338A" "-Wl,--wrap=sub_001A8750" "-Wl,--wrap=sub_001A8F60" "-Wl,--wrap=sub_0006D150" "-Wl,--wrap=sub_000E6D60" "-Wl,--wrap=sub_0018FDF0" "-Wl,--wrap=sub_000EB620" "-Wl,--wrap=sub_000ED720" "-Wl,--wrap=sub_000EC950")
        target_link_options(${target_name} PRIVATE "-Wl,--wrap=sub_0018565F")
        target_sources(${target_name} PRIVATE src/hair_pass_trace.c src/crowd_pose_trace.c)
        target_link_options(${target_name} PRIVATE "-Wl,--wrap=sub_00079AE0" "-Wl,--wrap=sub_00085450" "-Wl,--wrap=sub_0006BCA0" "-Wl,--wrap=sub_00086810")
    elseif(image STREQUAL "menu")
        target_sources(${target_name} PRIVATE src/menu_career_save_hooks.c src/menu_movie_capture.c src/menu_loading_ball_hooks.c)
        target_link_options(${target_name} PRIVATE "-Wl,--wrap=sub_000CB820" "-Wl,--wrap=sub_000348E0" "-Wl,--wrap=sub_001613A0")
        target_sources(${target_name} PRIVATE src/menu_video_hooks.c src/menu_canvas_hooks.c src/native_video.c src/menu_rui_trace.c src/menu_font_aspect_hooks.c src/menu_input_trace.c src/menu_loading_hooks.c src/loading_layout.c src/menu_movie_trace.c src/menu_movie_layout.c)
        # Graphics Options uses original RUI widgets/focus/animations; no overlay.
        target_sources(${target_name} PRIVATE src/menu_pc_entries.c src/native_graphics.c ${TOPSPIN_CONTROLLER_SESSION_SOURCE})
        target_include_directories(${target_name} PRIVATE ${SDK}/src ${SDK}/src/input)
        target_compile_definitions(${target_name} PRIVATE TOPSPIN_MENU_INPUT_OBSERVER=1 TOPSPIN_MENU_HOST=1 TOPSPIN_NATIVE_GRAPHICS=1)
        target_link_options(${target_name} PRIVATE "-Wl,--wrap=sub_0009E4E0" "-Wl,--wrap=sub_000CCE50" "-Wl,--wrap=sub_0015B180" "-Wl,--wrap=sub_00161650" "-Wl,--wrap=sub_000A67D0")
        target_link_options(${target_name} PRIVATE "-Wl,--wrap=sub_00162700" "-Wl,--wrap=sub_00155A80" "-Wl,--wrap=sub_000C98F0" "-Wl,--wrap=sub_0004CB00" "-Wl,--wrap=sub_0004C820" "-Wl,--wrap=sub_00137BA0" "-Wl,--wrap=sub_00137B80" "-Wl,--wrap=sub_0013B840")
    elseif(image STREQUAL "training" OR image STREQUAL "online")
        target_sources(${target_name} PRIVATE src/selector_video_hooks.c)
        if(image STREQUAL "training")
            target_sources(${target_name} PRIVATE src/training_rumble_hooks.c)
            target_compile_definitions(${target_name} PRIVATE TOPSPIN_SELECTOR_VIDEO_PROFILE=1)
            target_link_options(${target_name} PRIVATE "-Wl,--wrap=sub_00062550")
        else()
            target_compile_definitions(${target_name} PRIVATE TOPSPIN_SELECTOR_VIDEO_PROFILE=2)
            target_link_options(${target_name} PRIVATE "-Wl,--wrap=sub_000D7E90")
        endif()
    elseif(image MATCHES "^(boot|career|playercreator|shop|stop)$")
        target_sources(${target_name} PRIVATE src/mode_video_hooks.c src/native_video.c)
        if(image STREQUAL "boot")
            set(video_profile 2)
            set(video_wraps sub_00062FC0 sub_00057740 sub_0001C020)
        elseif(image STREQUAL "career")
            set(video_profile 3)
            set(video_wraps sub_00153890 sub_001472C0 sub_000E4A70)
        elseif(image STREQUAL "playercreator")
            target_sources(${target_name} PRIVATE src/playercreator_career_save_hooks.c src/creator_appearance_import.c src/appearance_equivalence.c src/playercreator_preview_hooks.c src/playercreator_material_read_trace.c src/playercreator_material_read_adapter.c src/material_coherence.c src/material_coherence_bridge.c src/material_producer_capture.c src/material_source_lock.c src/material_original_probe.c src/material_save_sources.c src/material_save_session.c src/material_coherence_startup.c src/playercreator_material_tracking_hooks.c ${TOPSPIN_SAVE_TRANSACTION_SOURCE} src/material_save_operation.c src/actor_binding_cpu_guard.c src/playercreator_save_operation_hooks.c src/playercreator_save_failure.c src/playercreator_model_checkpoint.c src/creator_ui_diagnostic.c src/creator_input_observer.c src/playercreator_material_export_hooks.c)
            target_compile_definitions(${target_name} PRIVATE TOPSPIN_PRIVATE_APPEARANCE_OBSERVER=1)
            set(video_profile 4)
            set(video_wraps sub_00164AD0 sub_001588E0 sub_00072760)
        elseif(image STREQUAL "shop")
            target_sources(${target_name} PRIVATE src/shop_preview_hooks.c)
            set(video_profile 5)
            set(video_wraps sub_0014F7B0 sub_00143780 sub_000DED80)
        else()
            set(video_profile 6)
            set(video_wraps sub_0005BF50 sub_00050A80 sub_0001DF90)
        endif()
        target_compile_definitions(${target_name} PRIVATE TOPSPIN_FRAMEWORK_VIDEO_PROFILE=${video_profile})
        foreach(symbol IN LISTS video_wraps)
            target_link_options(${target_name} PRIVATE "-Wl,--wrap=${symbol}")
        endforeach()
    endif()
    if(image MATCHES "^(menu|game|career|training|playercreator|shop|online)$")
        target_sources(${target_name} PRIVATE src/menu_prompt_runtime.c src/controller_rui_resources.c "src/prompt_hooks_${image}.c")
        target_compile_definitions(${target_name} PRIVATE TOPSPIN_PROMPT_IMAGE="${image}")
    endif()
    if(image MATCHES "^(game|career|training|playercreator|shop|online)$")
        target_sources(${target_name} PRIVATE "src/${image}_canvas_hooks.c" src/rui_canvas.c
            "src/${image}_font_aspect_hooks.c" src/rui_font_aspect.c)
        if(image STREQUAL "game")
            set(font_symbol sub_0012BCA0)
        elseif(image STREQUAL "career")
            set(font_symbol sub_000CF060)
        elseif(image STREQUAL "training")
            set(font_symbol sub_0018B750)
        elseif(image STREQUAL "playercreator")
            set(font_symbol sub_00144560)
        elseif(image STREQUAL "shop")
            set(font_symbol sub_00076110)
        else()
            set(font_symbol sub_0021DC90)
        endif()
        target_link_options(${target_name} PRIVATE "-Wl,--wrap=${font_symbol}")
    endif()
    set_target_properties(${target_name} PROPERTIES ENABLE_EXPORTS ON OUTPUT_NAME "topspin_${image}")
    target_include_directories(${target_name} PRIVATE ${SDK}/src/d3d ${SDK}/src/kernel src src/recomp/${image}
        src/recomp ${SDK}/include ${SDK}/src/platform ${SDK}/src/d3d ${SDK}/src/nv2a ${SDK}/src/apu)
    # Several runtime archives call each other; GNU ld must rescan the group.
    get_target_property(RUNTIME_ARCHIVES xboxrecomp INTERFACE_LINK_LIBRARIES)
    list(JOIN RUNTIME_ARCHIVES "," RUNTIME_ARCHIVES_GROUP)
    target_link_libraries(${target_name} PRIVATE "$<LINK_GROUP:RESCAN,${RUNTIME_ARCHIVES_GROUP}>"
        SDL3::SDL3 Vulkan::Vulkan ${CMAKE_DL_LIBS} ${TOPSPIN_MATH_LIBRARY} pthread)
    if(WIN32)
        target_sources(${target_name} PRIVATE "${ROOT}/windows/native/src/host_windows.c" "${MAXSPIN_WINDOWS_RUNTIME_DIR}/material_save_transaction_windows.c")
        target_sources(${target_name} PRIVATE "${ROOT}/windows/native/src/ui_layout_windows.c" "${ROOT}/windows/native/src/ui_layout_overlay.c" "${ROOT}/windows/native/src/character_windows.c")
        target_include_directories(${target_name} PRIVATE "${ROOT}/windows/native/src")
        target_compile_options(${target_name} PRIVATE -include "${ROOT}/windows/native/src/host_windows.h")
        target_link_options(${target_name} PRIVATE -Wl,--stack,8388608 -municode -Wl,--wrap=xbox_translate_path)
        target_link_libraries(${target_name} PRIVATE advapi32)
    endif()
    target_compile_definitions(${target_name} PRIVATE _GNU_SOURCE RECOMP_PGRAPH_FRAME_END_CALLBACK=1
        TOPSPIN_XBE_NAME="${XBE_NAME}")
    target_compile_options(${target_name} PRIVATE -fno-strict-aliasing -fwrapv -ffp-contract=off -g)
endfunction()
topspin_add_image("${TOPSPIN_IMAGE}" topspin_native)
foreach(image IN LISTS TOPSPIN_ADDITIONAL_IMAGES)
    if(NOT image STREQUAL TOPSPIN_IMAGE)
        topspin_add_image("${image}" "topspin_image_${image}")
    endif()
endforeach()
