# SPDX-License-Identifier: GPL-2.0-or-later
# SPDX-FileCopyrightText: Copyright (C) 2026 Max A. Vilkki a.k.a. codise
#
# Antti Lankila's original resid-fp (GPL-2.0-or-later), the one libsidplayfp 0.1.1 bundles
# in builders/residfp-builder/residfp/, from lib/residfp-2011 (a second clone of the
# libsidplayfp mirror, at tag 0.1.1). It's Engine::Lankila (DESIGN §2.4); our builder for
# libsidplayfp 3.x lives in src/core/engines (lankila_builder.h).
#
# Upstream builds with autotools. This mirrors libresidfp_a_SOURCES in its Makefile.am,
# minus convolve-sse.cc (RESID_USE_SSE is 0), and generated/ holds the siddefs-fp.h
# configure would make (see generated/README.md, refreshed by update_generated.sh).
#
# The 2011 code builds as it is, but has two bugs that matter for us, a slow loop and no
# output gain, so at configure time this copies the sources into the build tree (patched/)
# and patches the copies; every patch must match exactly as often as expected, or
# configuring stops. The clone stays untouched.
# - Per-chip tables. sid.cc keeps the waveform and DAC tables (dac, env_dac, wftable) in
#   globals that every chip reads each cycle, and set_chip_model() and
#   set_voice_nonlinearity() rebuild them. So a second chip, of the other model or with
#   other values (a mixed 6581+8580 2SID tune, an A/B comparison, a loudness scan on
#   another thread), changed the first chip's sound, and two threads raced on them. Now
#   each SIDFP owns a TablesFP (generated/siddefs-fp.h) and its voices point to it. Chosen
#   over a thread_local "current tables" pointer: no TLS access in the per-cycle path, and
#   no way to forget setting it.
# - fastexp() in filter.h builds a float from an int bit pattern. For arguments below
#   about -88 (a large positive filter state times a steep FET curve: above 7.5e6 at the
#   default steepness 1.006, 3.1e6 at Enigma's 1.0145) the int goes negative, the float
#   becomes a NaN and the chip stays silent for good. The tests reach it only at a corner of
#   the accepted ranges (every resistance lowest, steepness 1.05), no preset. The patch
#   clamps the argument at -87, where e^x is already 1.6e-38: the same result wherever the
#   original was right.
# - convolve() sums the resampler's FIR with eight running sums instead of one: 2.4 times
#   as fast at 48 kHz (Lankila Resample went from 11.7 to 28 times real time on Anal_ogue,
#   Release, Apple M-series, 2026-10-06; reSIDfp's Resample does 17). The order is fixed in
#   the source, so it's the same arithmetic on every compiler; 1664 of 2.88 million samples
#   (60 s at 48 kHz) came out 1 LSB different.
# - Sample phase: kept in double, so chips clocked in different pieces stay in step (see
#   the patch).
# - Output gain: SIDFP::output() multiplies by a per-chip output_gain (1 unless set), which
#   LankilaEmu sets per model to match reSIDfp's loudness. It has to be inside resid-fp:
#   clock_*() truncate and clamp each sample to 16 bits right after output().
#
# Namespaces: every source is compiled inside namespace residfp2011, through a small
# generated .cc per source (unity/) that includes <math.h> first and then the patched
# source inside the namespace. Users include <residfp2011/sid.h>, which does the same for
# the headers. So nothing global is left: SIDFP, FilterFP, convolve()... are
# residfp2011::, and resid_version_string has C++ linkage (in C linkage it was classic
# reSID's symbol too).
#
# Not built: the SSE convolution and its CPU detection (host_cpu_features(), lazily
# initialised statics, a data race between chips created on two threads, and 32-bit x86
# assembly). The plain C++ convolution, patched as above, does the job on every CPU.
#
# Target:
#   ext::residfp2011   the library; users include <residfp2011/sid.h>.
#
# Size (Release, AppleClang 17, arm64, 2026-10-06, from the linker map of the flipsid
# app): 13.5 KB of resid-fp code and constants, 3.8 KB for our builder and emu, and 7.8 KB
# for the preset table (lankila_presets.cpp): 25 KB in all. No large static tables: each
# chip's TablesFP (181 KB) and its FIR tables (72 KB at 44.1 kHz) are on the heap.

if(NOT DEFINED FLIPSID_LIB_DIR)
    set(FLIPSID_LIB_DIR "${CMAKE_CURRENT_LIST_DIR}/../..")
endif()
set(residfp2011Dir "${FLIPSID_LIB_DIR}/residfp-2011/builders/residfp-builder/residfp")
set(residfp2011Generated "${CMAKE_CURRENT_LIST_DIR}/generated")

if(NOT EXISTS "${residfp2011Dir}/siddefs-fp.h.in")
    message(FATAL_ERROR
        "resid-fp 2011: sources not found in ${residfp2011Dir}.\n"
        "Run lib/fetch_externals.sh (lib\\fetch_externals.cmd on Windows), then configure again.")
endif()

# libresidfp_a_SOURCES (Makefile.am) without convolve-sse.cc, and noinst_HEADERS.
set(residfp2011Sources sid.cc voice.cc wave.cc envelope.cc filter.cc extfilt.cc pot.cc version.cc convolve.cc)
set(residfp2011Headers sid.h voice.h wave.h envelope.h filter.h extfilt.h pot.h)

set(residfp2011Watched
    "${residfp2011Dir}/siddefs-fp.h.in"
    "${residfp2011Dir}/Makefile.am"
    "${residfp2011Generated}/siddefs-fp.h"
)
foreach(file IN LISTS residfp2011Sources residfp2011Headers)
    list(APPEND residfp2011Watched "${residfp2011Dir}/${file}")
endforeach()
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${residfp2011Watched})

# generated/siddefs-fp.h has to come from the clone's siddefs-fp.h.in. Everything read
# from the clone is normalised to LF first: the clone has no .gitattributes, so Git for
# Windows (core.autocrlf=true by default) checks it out with CRLF, and the hash and every
# patch below match LF text.
file(READ "${residfp2011Dir}/siddefs-fp.h.in" residfp2011Template)
string(REPLACE "\r\n" "\n" residfp2011Template "${residfp2011Template}")
string(SHA256 residfp2011TemplateHash "${residfp2011Template}")
file(READ "${residfp2011Generated}/siddefs-fp.h" residfp2011Siddefs)
if(NOT residfp2011Siddefs MATCHES "sha256 ${residfp2011TemplateHash}")
    message(FATAL_ERROR
        "resid-fp 2011: lib/cmake/residfp-2011/generated/siddefs-fp.h wasn't made from "
        "${residfp2011Dir}/siddefs-fp.h.in.\n"
        "Run lib/cmake/residfp-2011/update_generated.sh and check Makefile.am's source list "
        "against lib/cmake/residfp-2011/CMakeLists.txt.")
endif()

# The source list has to match upstream's, so a new file in a new pin isn't left out.
file(READ "${residfp2011Dir}/Makefile.am" residfp2011MakefileAm)
string(REPLACE "\r\n" "\n" residfp2011MakefileAm "${residfp2011MakefileAm}")
if(NOT residfp2011MakefileAm MATCHES
        "libresidfp_a_SOURCES = sid.cc voice.cc wave.cc envelope.cc filter.cc extfilt.cc pot.cc version.cc convolve.cc\n")
    message(FATAL_ERROR
        "resid-fp 2011: libresidfp_a_SOURCES in Makefile.am changed; update "
        "lib/cmake/residfp-2011/CMakeLists.txt.")
endif()

# Writes content to path unless it's there already, so reconfiguring doesn't rebuild.
function(residfp2011_write path content)
    if(EXISTS "${path}")
        file(READ "${path}" old)
        if(old STREQUAL content)
            return()
        endif()
    endif()
    file(WRITE "${path}" "${content}")
endfunction()

# residfp2011_patch(<file> <count> <before> <after>): replaces before with after in the
# variable named by `file` (residfp2011_<name>), which must hold it exactly count times.
function(residfp2011_patch file count before after)
    set(text "${residfp2011_${file}}")
    string(LENGTH "${text}" length)
    string(REPLACE "${before}" "" without "${text}")
    string(LENGTH "${without}" lengthWithout)
    string(LENGTH "${before}" beforeLength)
    math(EXPR found "(${length} - ${lengthWithout}) / ${beforeLength}")
    if(NOT found EQUAL count)
        message(FATAL_ERROR
            "resid-fp 2011: expected ${count} of\n${before}\nin ${file}, found ${found}. "
            "The pinned sources changed; update the patches in "
            "lib/cmake/residfp-2011/CMakeLists.txt.")
    endif()
    string(REPLACE "${before}" "${after}" text "${text}")
    set(residfp2011_${file} "${text}" PARENT_SCOPE)
endfunction()

foreach(file IN LISTS residfp2011Sources residfp2011Headers)
    file(READ "${residfp2011Dir}/${file}" residfp2011_${file})
    string(REPLACE "\r\n" "\n" residfp2011_${file} "${residfp2011_${file}}")
endforeach()

set(mark "// flipSID (lib/cmake/residfp-2011): ")

# Per-chip tables: the globals go, each SIDFP gets a TablesFP, and its voices' waveform and
# envelope generators point to it.
residfp2011_patch(sid.cc 1
    "float dac[12];\nfloat env_dac[256];\nfloat wftable[11][4096];\n"
    "${mark}the tables are per chip, in SIDFP::tables.\n")
residfp2011_patch(sid.h 1
    "  bool can_use_sse;\n"
    "  bool can_use_sse;\n\n  ${mark}this chip's tables; the voices point to them.\n  TablesFP tables{};\n")
residfp2011_patch(sid.cc 1
    "  // Initialize pointers.\n  sample = 0;\n"
    "  ${mark}every voice uses this chip's tables.\n  for (int i = 0; i < 3; i++) {\n    voice[i].wave.tables = &tables;\n    voice[i].envelope.tables = &tables;\n  }\n\n  // Initialize pointers.\n  sample = 0;\n")
residfp2011_patch(wave.h 1
    "extern float dac[12];\nextern float wftable[11][4096];\n"
    "${mark}dac and wftable are in the chip's TablesFP.\n")
residfp2011_patch(wave.h 1
    "  float previous_dac;\n\nfriend class SIDFP;"
    "  float previous_dac;\n\n  ${mark}the chip's tables, set by SIDFP.\n  TablesFP* tables = nullptr;\n\nfriend class SIDFP;")
residfp2011_patch(wave.h 1
    "return wftable[waveform + variant][phase >> 12];"
    "return tables->wftable[waveform + variant][phase >> 12];")
residfp2011_patch(wave.cc 1
    "static float make_sample(float *o) {"
    "static float make_sample(const float *dac, float *o) {")
residfp2011_patch(wave.cc 2 "make_sample(o)" "make_sample(tables->dac, o)")
residfp2011_patch(wave.cc 1 "wftable[waveform - 1]" "tables->wftable[waveform - 1]")
residfp2011_patch(wave.cc 1 "wftable[waveform + 3]" "tables->wftable[waveform + 3]")
residfp2011_patch(wave.cc 1
    "    dac[i] = SIDFP::kinked_dac((1 << i), nl, 12);"
    "    tables->dac[i] = SIDFP::kinked_dac((1 << i), nl, 12);")
residfp2011_patch(wave.cc 1 "previous_dac += dac[i+4];" "previous_dac += tables->dac[i+4];")
residfp2011_patch(envelope.h 1
    "extern float env_dac[256];\n"
    "${mark}env_dac is in the chip's TablesFP.\n")
residfp2011_patch(envelope.h 1
    "env_dac[envelope_counter]"
    "tables->env_dac[envelope_counter]")
residfp2011_patch(envelope.h 1
    "  static reg8 sustain_level[];\n"
    "  static reg8 sustain_level[];\n\n  ${mark}the chip's tables, set by SIDFP.\n  TablesFP* tables = nullptr;\n")
residfp2011_patch(envelope.cc 1
    "        env_dac[i] = SIDFP::kinked_dac(i, nl, 8);"
    "        tables->env_dac[i] = SIDFP::kinked_dac(i, nl, 8);")

# Output gain: output() scales by a per-chip gain, before clock_*() truncate and clamp the
# sample to 16 bits, so the gain also keeps peaks from clipping. A gain after the emu
# couldn't undo that clamp. LankilaEmu sets it from the chip model (lankila_emu.cpp).
residfp2011_patch(sid.h 1
    "  TablesFP tables{};\n"
    "  TablesFP tables{};\n\n  ${mark}scales output(), see SIDFP::output().\n  float output_gain = 1.f;\n")
residfp2011_patch(sid.cc 1
    "  return extfilt.output() / (2047.f * 255.f * 3.f * 2.0f / range);\n"
    "  ${mark}times the chip's output gain, before the caller clamps to 16 bits.\n  return extfilt.output() * output_gain / (2047.f * 255.f * 3.f * 2.0f / range);\n")

# Sample phase in double. sample_offset (and clock_*()'s next_sample_offset) are float,
# so where phase + cycles_per_sample reaches the next power of two it's rounded, and how
# often depends on how the chip is clocked: in one chunk, or in pieces split by register
# accesses. Two chips of a 2SID or 3SID tune then drift apart and return different sample
# counts from the same cycles (NTSC at 32, 64 and 128 kHz: about 1 call in 7), but
# libsidplayfp's Player::play() takes the count from the last chip only and mixes that
# many samples from every chip's buffer: one sample dropped or a stale one read. In double,
# every step lands exactly on cycles_per_sample's float grid, whatever the chunks.
residfp2011_patch(sid.h 1 "  float sample_offset;" "  double sample_offset;")
residfp2011_patch(sid.cc 2 "float next_sample_offset =" "double next_sample_offset =")

# fastexp(): no NaN from arguments below the float exponent's range (see above).
residfp2011_patch(filter.h 1
    "    tmp.i = static_cast<int>(a * val + (b - c + 0.5f));"
    "    ${mark}below -87 the int would go negative and the float a NaN.\n    if (val < -87.f) {\n        val = -87.f;\n    }\n    tmp.i = static_cast<int>(a * val + (b - c + 0.5f));")

# convolve(): eight running sums instead of one. The resampler runs it twice per output
# sample over the whole FIR (780 taps at 48 kHz, 1385 at 44.1 kHz), and a single float sum
# waits for each addition in turn, which compilers may not reorder without -ffast-math.
# Eight independent sums, added up in a fixed order, are the same arithmetic on every
# compiler, and vectorise.
residfp2011_patch(convolve.cc 1
    "float convolve(const float *a, const float *b, int n)\n{\n    float out = 0.f;\n    while (n --)\n        out += (*(a ++)) * (*(b ++));\n    return out;\n}\n"
    "${mark}eight running sums, for speed (see there).\nfloat convolve(const float *a, const float *b, int n)\n{\n    float sum[8] = {0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f};\n    int i = 0;\n    for (; i + 8 <= n; i += 8) {\n        for (int j = 0; j < 8; j++) {\n            sum[j] += a[i + j] * b[i + j];\n        }\n    }\n    for (; i < n; i++) {\n        sum[i & 7] += a[i] * b[i];\n    }\n    return ((sum[0] + sum[1]) + (sum[2] + sum[3])) + ((sum[4] + sum[5]) + (sum[6] + sum[7]));\n}\n")

# No table use may be left unpatched: anything indexing dac, env_dac or wftable must go
# through the chip's tables (make_sample()'s own dac parameter aside).
foreach(file IN LISTS residfp2011Sources residfp2011Headers)
    set(text "${residfp2011_${file}}")
    string(REGEX REPLACE "tables->(dac|env_dac|wftable)\\[" "" text "${text}")
    string(REPLACE "out += o[i] * dac[i];" "" text "${text}")
    if(text MATCHES "(^|[^A-Za-z0-9_])(dac|env_dac|wftable)\\[")
        message(FATAL_ERROR
            "resid-fp 2011: ${file} uses a table the patches don't cover; update "
            "lib/cmake/residfp-2011/CMakeLists.txt.")
    endif()
endforeach()

set(residfp2011Patched "${CMAKE_CURRENT_BINARY_DIR}/patched")
foreach(file IN LISTS residfp2011Sources residfp2011Headers)
    residfp2011_write("${residfp2011Patched}/${file}" "${residfp2011_${file}}")
endforeach()
configure_file("${residfp2011Generated}/siddefs-fp.h" "${residfp2011Patched}/siddefs-fp.h" COPYONLY)

# The sources inside the namespace. <math.h> first, outside it: filter.h, extfilt.h and
# sid.cc include it, and its include guard then keeps the C library out of the namespace.
set(residfp2011Unity "${CMAKE_CURRENT_BINARY_DIR}/unity")
set(residfp2011UnitySources "")
foreach(file IN LISTS residfp2011Sources)
    residfp2011_write("${residfp2011Unity}/${file}"
        "// Generated by lib/cmake/residfp-2011/CMakeLists.txt: resid-fp's ${file} in namespace residfp2011.\n#include <math.h>\n\nnamespace residfp2011 {\n#include \"../patched/${file}\"\n} // namespace residfp2011\n")
    list(APPEND residfp2011UnitySources "${residfp2011Unity}/${file}")
endforeach()

set(residfp2011Include "${CMAKE_CURRENT_BINARY_DIR}/include")
residfp2011_write("${residfp2011Include}/residfp2011/sid.h"
    "// Generated by lib/cmake/residfp-2011/CMakeLists.txt: resid-fp's SIDFP (libsidplayfp 0.1.1),\n// with everything it declares, in namespace residfp2011.\n#pragma once\n\n#include <math.h>\n\nnamespace residfp2011 {\n#include \"../../patched/sid.h\"\n} // namespace residfp2011\n")

add_library(ext_residfp2011 STATIC ${residfp2011UnitySources})
add_library(ext::residfp2011 ALIAS ext_residfp2011)

target_compile_features(ext_residfp2011 PRIVATE cxx_std_20)
set_target_properties(ext_residfp2011 PROPERTIES
    OUTPUT_NAME residfp2011
    CXX_EXTENSIONS OFF
    CXX_VISIBILITY_PRESET hidden
    VISIBILITY_INLINES_HIDDEN ON
)
target_include_directories(ext_residfp2011 SYSTEM INTERFACE "${residfp2011Include}")

# M_PI and M_LN2: siddefs-fp.h defines them when math.h doesn't (MSVC without
# _USE_MATH_DEFINES).
if(MSVC)
    # /w: third-party warnings aren't ours to fix.
    target_compile_options(ext_residfp2011 PRIVATE /w)
else()
    target_compile_options(ext_residfp2011 PRIVATE -w)
endif()
# Not replicated from configure: -O3 -ffast-math (fast-math lets the compiler reorder the
# filter's float arithmetic, so the sound would depend on the compiler; the spike was
# heard without it), -msse and -fno-exceptions (std::bad_alloc from the FIR tables has to
# reach the builder).

if(COMMAND flipsid_configure_third_party)
    flipsid_configure_third_party(ext_residfp2011)
endif()
