diff --git a/CHANGELOG.md b/CHANGELOG.md index 870b945..4e38899 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,14 @@ All notable changes to CableDyn are recorded in this file. The format follows ### Changed +- The Windows release executables (`CableDyn_driver.exe`, `openfast.exe`) are compiled for + processors with AVX2 (x86-64-v3: Intel Haswell and AMD Excavator or newer; CMake option + `CABLEDYN_TARGET_AVX2`), which makes the driver 13-33% faster on the benchmark decks + (`lazy_wave_vessel_rao` 11.9 s to 8.8 s, `als_volturnus_line_break_time` 35.5 s to 23.9 s). + On an older processor they stop at start with a message naming the missing instructions. + Results change at round-off: in the last printed digit on the lazy-wave and buoy decks; in the + line-break deck the platform's slow drift after the break amplifies the round-off past 900 s, + with the same peaks and standard deviations to four digits. - OpenFAST: `CalcOutput` no longer evaluates the reactions of the finite-EI cables when it saves the committed kinematics before transferring the current ones; it discarded them. With a 248-element power cable in the IEA-15 VolturnUS-S mooring this cuts `CalcOutput` from 41 s to diff --git a/CMakeLists.txt b/CMakeLists.txt index 551c910..27f4816 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,9 @@ option(CABLEDYN_ENABLE_COVERAGE "Instrument the GNU Fortran core for gcov/gcovr option(CABLEDYN_ENABLE_OPENMP "Enable OpenMP acceleration when the compiler provides it" ON) option(CABLEDYN_STATIC_WINDOWS_RELEASE "Build a single-file non-OpenMP Windows release with Intel IFX/MSVC" OFF) +option(CABLEDYN_TARGET_AVX2 + "Compile the Fortran core for x86-64-v3 (AVX2, FMA, BMI1/2); programs then stop with a message on older processors" + OFF) if(CABLEDYN_STATIC_WINDOWS_RELEASE) if(NOT WIN32 OR NOT CMAKE_Fortran_COMPILER_ID STREQUAL "IntelLLVM" OR NOT MSVC) @@ -25,6 +28,8 @@ if(CABLEDYN_STATIC_WINDOWS_RELEASE) # static MSVC CRT, and no OpenMP runtime dependency (IFX has no static OpenMP # runtime on Windows). set(CABLEDYN_ENABLE_OPENMP OFF CACHE BOOL "" FORCE) + # The release executables target x86-64-v3 (CABLEDYN_TARGET_AVX2 below). + set(CABLEDYN_TARGET_AVX2 ON CACHE BOOL "" FORCE) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded" CACHE STRING "" FORCE) # Large finite-EI meshes create compiler-generated array temporaries in deeply # nested residual/assembly calls. Keep those temporaries off the process stack; @@ -63,6 +68,39 @@ if(CABLEDYN_STATIC_WINDOWS_RELEASE) endforeach() endif() +# x86-64-v3 (Intel Haswell, AMD Excavator and newer): the vector maths of the wave +# kinematics and the element kernels run four doubles wide instead of two. On the release +# benchmark decks the IFX driver is 13-33% faster; results change at round-off (the wider +# vector maths), identically on every x86-64-v3 processor. Contraction into fused +# multiply-adds stays off: the compiler would fuse the same expression differently where it +# is inlined, so a restored or re-entered state would no longer continue a run bit for bit +# (test_line_attachments), for 2-5% more speed. IFX takes the /arch form, documented for +# any processor with these instructions (the /Qx form is documented for Intel processors; +# both gave the same code and speed on the benchmarks). +# Only the Fortran sources take it: src/cabledyn_cpu_guard.c stays baseline x86-64 and +# stops a program on an older processor with a message before any Fortran code runs. +if(CABLEDYN_TARGET_AVX2) + if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$") + message(FATAL_ERROR "CABLEDYN_TARGET_AVX2 needs an x86-64 target (found ${CMAKE_SYSTEM_PROCESSOR})") + endif() + if(CMAKE_Fortran_COMPILER_ID STREQUAL "IntelLLVM") + if(WIN32) + string(APPEND CMAKE_Fortran_FLAGS " /arch:CORE-AVX2 /Qfma-") + else() + string(APPEND CMAKE_Fortran_FLAGS " -march=core-avx2 -no-fma") + endif() + elseif(CMAKE_Fortran_COMPILER_ID STREQUAL "GNU") + if(CMAKE_Fortran_COMPILER_VERSION VERSION_LESS 11) + message(FATAL_ERROR "CABLEDYN_TARGET_AVX2 needs GNU Fortran 11 or newer (-march=x86-64-v3)") + endif() + string(APPEND CMAKE_Fortran_FLAGS " -march=x86-64-v3 -ffp-contract=off") + else() + message(FATAL_ERROR "CABLEDYN_TARGET_AVX2 supports Intel IFX and GNU Fortran") + endif() + set_source_files_properties(src/cabledyn_cpu_guard.c PROPERTIES + COMPILE_DEFINITIONS CABLEDYN_REQUIRE_X86_64_V3) +endif() + # src/cabledyn_mutex.c uses C11 , which the MSVC C compiler provides only # behind this flag, in every MSVC configuration (static release or not). if(CMAKE_C_COMPILER_ID STREQUAL "MSVC") @@ -328,6 +366,7 @@ add_library(cabledyn_obj OBJECT src/cabledyn_mutex.c src/cabledyn_blas.c src/cabledyn_crt_locale.c + src/cabledyn_cpu_guard.c src/cabledyn_path.c src/cabledyn_fatal.c src/CableDyn_Precision.f90 @@ -1996,6 +2035,21 @@ add_executable(test_crt_locale_guard tests/test_crt_locale_guard.f90 tests/crt_l target_link_libraries(test_crt_locale_guard PRIVATE cabledyn_core) add_test(NAME crt_locale_guard COMMAND test_crt_locale_guard $,1,0>) set_tests_properties(crt_locale_guard PROPERTIES LABELS "fortran;unit") +# The x86-64-v3 start-up processor check is armed exactly in CABLEDYN_TARGET_AVX2 builds. +add_executable(test_cpu_guard tests/test_cpu_guard.f90) +target_link_libraries(test_cpu_guard PRIVATE cabledyn_core) +add_test(NAME cpu_guard COMMAND test_cpu_guard $,1,0>) +set_tests_properties(cpu_guard PROPERTIES LABELS "fortran;unit") +if(CABLEDYN_TARGET_AVX2) + # The armed check stops the driver before any Fortran code runs, with the message naming + # what is missing (a simulated feature, so the stop is exercised on any processor). + add_test(NAME cpu_guard_stop + COMMAND ${CMAKE_COMMAND} -E env CABLEDYN_CPU_GUARD_SIMULATE=SIMULATED-FEATURE + $ --version) + set_tests_properties(cpu_guard_stop PROPERTIES LABELS "fortran;unit" + PASS_REGULAR_EXPRESSION "This computer lacks: SIMULATED-FEATURE" + FAIL_REGULAR_EXPRESSION "CableDyn v") +endif() # An abnormal end of the driver process is never silent: a stack overflow (on a 1 MiB stack # in Windows GNU builds), an access violation and, off Windows, SIGTERM each end the process # with a report on stderr of the cause and the simulated time reached; the march records the diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 0653a8f..287bb9a 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -44,6 +44,18 @@ The Debug configuration (`-DCMAKE_BUILD_TYPE=Debug`) adds `-fcheck=all conda OpenBLAS through `CONDA_PREFIX`. `cmake --install build --prefix stage` installs the driver, the shared library, and `CableDyn_CAPI.h`. +`-DCABLEDYN_TARGET_AVX2=ON` compiles the Fortran core for x86-64-v3 (AVX2, FMA, +BMI1/2: Intel Haswell and AMD Excavator or newer) with IFX (`/arch:CORE-AVX2`) or +gfortran 11+ (`-march=x86-64-v3`), with contraction into fused multiply-adds off +(`/Qfma-`, `-ffp-contract=off`) so a restored state still continues a run bit for +bit. The static Windows release always sets it: its IFX driver runs the benchmark +decks 13-33% faster, and the results change at round-off. Such a build checks the +processor when it starts +(`src/cabledyn_cpu_guard.c`, compiled for baseline x86-64) and stops with a +message naming the missing instructions instead of crashing on the first one; +the driver's `--version` output and the OpenFAST module's start-up lines report the +x86-64-v3 build. The default (`OFF`) runs on any x86-64 processor. + A Windows build with gfortran links every executable with a 64 MiB stack reserve (the MinGW default is 2 MiB). With OpenMP enabled, gfortran keeps fixed-size local arrays on the stack, and the stack depth of the OpenBLAS kernels differs between diff --git a/README.md b/README.md index b5b9f44..edc103a 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ a named error rather than running approximately. | Asset | Contents | |---|---| - | `CableDyn_driver.exe` | Standalone solver for Windows x64 | + | `CableDyn_driver.exe` | Standalone solver for Windows x64 (AVX2 processor) | | `openfast.exe` | OpenFAST with CableDyn available as `CompMooring = 5` | | `cabledyn-0.2.0-py3-none-any.whl` | Python package (pre- and post-processing, batch runs) | | `cabledyn-0.2.0.tar.gz` | Python source distribution | diff --git a/app/cabledyn.f90 b/app/cabledyn.f90 index bcb9978..3e2b9fb 100644 --- a/app/cabledyn.f90 +++ b/app/cabledyn.f90 @@ -44,6 +44,7 @@ PROGRAM cabledyn CD_HermiteCable_Dyn_Recovery_Reset, CD_HermiteCable_Dyn_Recovery_Count, & CD_HermiteCable_Dyn_Get_Assembly_Profile, HC_ASM_NSEC, HC_ASM_NAMES USE, INTRINSIC :: ISO_FORTRAN_ENV, ONLY: error_unit, output_unit, int64 + USE, INTRINSIC :: ISO_C_BINDING, ONLY: C_INT IMPLICIT NONE TYPE :: StandaloneProgress INTEGER :: nstep = 0, stride = 1 @@ -56,6 +57,14 @@ PROGRAM cabledyn CHARACTER(*), PARAMETER :: PROG = 'CableDyn_driver' ! Lock file beside the outputs that keeps a second run off the same output root. CHARACTER(*), PARAMETER :: LOCK_SUFFIX = '.cabledyn.lock' + INTERFACE + ! 1 when this build targets x86-64-v3 and carries the start-up processor check of + ! src/cabledyn_cpu_guard.c (the reference also keeps that check in the static link) + FUNCTION cpu_guard_armed() BIND(C, name='cabledyn_cpu_guard_armed') RESULT(armed) + IMPORT :: C_INT + INTEGER(C_INT) :: armed + END FUNCTION cpu_guard_armed + END INTERFACE ! Longest suffix a run appends to the output root (".Line.range.out" with a 10-digit id). INTEGER, PARAMETER :: MAX_OUTPUT_SUFFIX = 25 ! Command-line paths as UTF-8; the native spellings are what the Fortran runtime opens. @@ -80,6 +89,11 @@ PROGRAM cabledyn SELECT CASE (TRIM(deck_path)) CASE ('-v', '-V', '-version', '-VERSION', '--version') CALL CD_Print_Banner() + IF (cpu_guard_armed() /= 0) THEN + WRITE (output_unit, '(A)') ' Build x86-64-v3 (AVX2)' + ELSE + WRITE (output_unit, '(A)') ' Build baseline' + END IF STOP 0, QUIET = .TRUE. CASE ('-h', '-H', '-help', '-HELP', '--help', '-?', '/?') CALL CD_Print_Banner() diff --git a/doc/installation.rst b/doc/installation.rst index b7d78cb..6b6eea9 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -52,6 +52,14 @@ What is on the release page the ``examples/`` folder of the **Source code (zip)** archive on the same release page. Download it once and unzip it next to the executables. +.. note:: + + The executables need a 64-bit processor with AVX2 (the x86-64-v3 level: Intel Haswell, + 2013, and AMD Excavator, 2015, or newer), for which they are compiled. On an older processor + they stop at start with a message that names the missing instructions; there, build CableDyn + from source as `DEVELOPMENT.md + `_ describes. + Download and verify ~~~~~~~~~~~~~~~~~~~ diff --git a/release/build_static_windows.ps1 b/release/build_static_windows.ps1 index 28ee74e..8a72f4a 100644 --- a/release/build_static_windows.ps1 +++ b/release/build_static_windows.ps1 @@ -123,6 +123,7 @@ $common = '-G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_Fortran_COMPILER=ifx ' + # so both executables link the netlib reference LAPACK/BLAS, compiled here with the # release IFX from a pinned, checksummed source archive. /fp:precise keeps IEEE # semantics inside LAPACK; the upstream CMake applies a strict model only on Unix. +# /arch:CORE-AVX2 is the x86-64-v3 target of the CableDyn core (CABLEDYN_TARGET_AVX2). $lapackVersion = '3.12.1' $lapackSha256 = '2ca6407a001a474d4d4d35f3a61550156050c48016d949f0da0529c0aa052422' $lapackUrl = "https://github.com/Reference-LAPACK/lapack/archive/refs/tags/v$lapackVersion.tar.gz" @@ -160,7 +161,7 @@ Invoke-StaticToolchain ("cmake -S `"$lapackSource`" -B `"$lapackBuild`" $common '-DBUILD_SHARED_LIBS=OFF -DBUILD_TESTING=OFF -DBUILD_SINGLE=OFF -DBUILD_DOUBLE=ON ' + '-DBUILD_COMPLEX=OFF -DBUILD_COMPLEX16=OFF -DBUILD_INDEX64_EXT_API=OFF ' + '-DUSE_OPTIMIZED_BLAS=OFF -DUSE_OPTIMIZED_LAPACK=OFF ' + - '"-DCMAKE_Fortran_FLAGS=/nologo /libs:static /fp:precise" ' + + '"-DCMAKE_Fortran_FLAGS=/nologo /libs:static /fp:precise /arch:CORE-AVX2" ' + "`"-DCMAKE_INSTALL_PREFIX=$lapackInstall`"") Invoke-StaticToolchain "cmake --build `"$lapackBuild`" --target blas lapack -j $Jobs" Invoke-StaticToolchain "cmake --install `"$lapackBuild`"" @@ -220,8 +221,28 @@ Copy-Item -LiteralPath $vsOpenFAST -Destination $openfast -Force $savedPath = $env:PATH $env:PATH = "$env:SystemRoot\System32;$env:SystemRoot" try { - & $driver --version - if ($LASTEXITCODE -ne 0) { throw 'CableDyn_driver.exe --version failed in isolated PATH' } + $versionRun = Invoke-NativeCapture $driver @('--version') + Write-Host $versionRun.Text + if ($versionRun.ExitCode -ne 0) { throw 'CableDyn_driver.exe --version failed in isolated PATH' } + # the release targets x86-64-v3 and carries the start-up processor check + if ($versionRun.Text -notmatch 'Build\s+x86-64-v3') { + throw "CableDyn_driver.exe is not the x86-64-v3 build with the processor check`n$($versionRun.Text)" + } + # On a processor without x86-64-v3 both executables stop at start with the message + # (simulated here): exit 3 before any CableDyn or OpenFAST code prints. + $env:CABLEDYN_CPU_GUARD_SIMULATE = 'SIMULATED-FEATURE' + try { + foreach ($stopCase in @(@($driver, '--version'), @($openfast, '-h'))) { + $stopRun = Invoke-NativeCapture $stopCase[0] @($stopCase[1]) + if ($stopRun.ExitCode -ne 3 -or $stopRun.Text -notmatch 'This computer lacks: SIMULATED-FEATURE' -or + $stopRun.Text -match 'CableDyn v|OpenFAST') { + throw "$($stopCase[0]) did not stop at start on a processor without x86-64-v3 " + + "(exit $($stopRun.ExitCode))`n$($stopRun.Text)" + } + } + } finally { + Remove-Item Env:CABLEDYN_CPU_GUARD_SIMULATE -ErrorAction SilentlyContinue + } $driverRoot = Join-Path $cdBuild 'driver_static_smoke' & $driver (Join-Path $CableDynRoot 'examples\chain_catenary_shallow_30m.dat') $driverRoot if ($LASTEXITCODE -ne 0) { throw 'CableDyn_driver.exe static smoke case failed' } @@ -399,6 +420,9 @@ try { if ($runLog -notmatch 'Running CableDyn \(' -or $runLog -notmatch 'OpenFAST terminated normally') { throw "openfast.exe did not prove CableDyn identity and normal termination`n$runLog" } + if ($runLog -notmatch 'CableDyn build: x86-64-v3') { + throw "openfast.exe does not carry the x86-64-v3 CableDyn core with the processor check`n$runLog" + } if ($runLog -notmatch 'Created CableDyn model:' -or $runLog -notmatch 'Line 1 fairlead effective tension:' -or $runLog -notmatch ('Requested CableDyn OUTPUTS at t = 0 s \(equilibrium pose, SeaState ' + diff --git a/src/cabledyn_cpu_guard.c b/src/cabledyn_cpu_guard.c new file mode 100644 index 0000000..d3d98be --- /dev/null +++ b/src/cabledyn_cpu_guard.c @@ -0,0 +1,165 @@ +/* File: src/cabledyn_cpu_guard.c + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2026 Jae Hoon Seo, SMI Lab, Inha University + * + * Start-up check of the processor for builds that target x86-64-v3 (CABLEDYN_TARGET_AVX2). + * + * Such a build lets the Fortran compiler and its vector maths use AVX2, FMA, BMI1/BMI2, LZCNT, + * MOVBE and F16C anywhere, so on an older processor it would stop at its first such + * instruction with an illegal-instruction fault and no explanation. This file is compiled without those + * instructions; when CABLEDYN_REQUIRE_X86_64_V3 is defined it registers a load-time + * initializer (a CRT initializer with MSVC-style toolchains, a constructor with GNU-style + * ones) that runs before any Fortran code of the executable or shared library. On a + * processor (or operating system) without the x86-64-v3 features it writes which are + * missing to standard error and ends the process with status 3. + * + * The message is written with the operating system's own calls and no C library string or + * stdio function: in the Windows GNU build those names can resolve to the same exports of + * openblas.dll, which would then be imported at load time (CABLEDYN_BLAS_RUNTIME_LOAD loads + * it on first use instead). + * + * CABLEDYN_CPU_GUARD_SIMULATE, when set to a non-empty value in an armed build, is reported + * as a missing feature, so the release gates and the tests can exercise the stop on any + * processor. + */ + +/* Writes the missing x86-64-v3 features, space-separated, to buf (empty when none is + * missing or the target is not x86-64) and returns their number. */ +int cabledyn_cpu_missing_features(char *buf, int len); +/* 1 when this build carries the start-up check, 0 otherwise. */ +int cabledyn_cpu_guard_armed(void); + +/* Appends text (and a separating space when buf is not empty) if it fits with the + * terminator; returns 1 when it did. */ +static int append(char *buf, int len, const char *text, int separate) { + int used = 0, n = 0, i; + if (!buf || len <= 0) return 0; + while (used < len && buf[used]) ++used; + while (text[n]) ++n; + if (separate && used > 0) { + if (used + 1 + n + 1 > len) return 0; + buf[used++] = ' '; + } else if (used + n + 1 > len) { + return 0; + } + for (i = 0; i < n; ++i) buf[used + i] = text[i]; + buf[used + n] = '\0'; + return 1; +} + +#if defined(_M_X64) || defined(__x86_64__) +#if defined(_MSC_VER) +#include +static void cpuid_count(unsigned leaf, unsigned sub, unsigned r[4]) { + int v[4]; + __cpuidex(v, (int)leaf, (int)sub); + r[0] = (unsigned)v[0]; r[1] = (unsigned)v[1]; r[2] = (unsigned)v[2]; r[3] = (unsigned)v[3]; +} +static unsigned long long read_xcr0(void) { return _xgetbv(0); } +#else +#include +static void cpuid_count(unsigned leaf, unsigned sub, unsigned r[4]) { + __cpuid_count(leaf, sub, r[0], r[1], r[2], r[3]); +} +static unsigned long long read_xcr0(void) { + unsigned lo, hi; + __asm__ volatile("xgetbv" : "=a"(lo), "=d"(hi) : "c"(0)); + return ((unsigned long long)hi << 32) | lo; +} +#endif + +static void note(char *buf, int len, int *count, const char *name) { + ++*count; + append(buf, len, name, 1); +} + +int cabledyn_cpu_missing_features(char *buf, int len) { + unsigned r0[4], r1[4], r7[4] = {0, 0, 0, 0}, rx[4] = {0, 0, 0, 0}; + int count = 0, os_avx = 0; + if (buf && len > 0) buf[0] = '\0'; + cpuid_count(0, 0, r0); + cpuid_count(1, 0, r1); + if (r0[0] >= 7) cpuid_count(7, 0, r7); + cpuid_count(0x80000000u, 0, rx); + if (rx[0] >= 0x80000001u) cpuid_count(0x80000001u, 0, rx); + else rx[2] = 0; + /* AVX state enabled by the operating system: OSXSAVE, then XMM and YMM in XCR0 */ + if (r1[2] & (1u << 27)) os_avx = (read_xcr0() & 6u) == 6u; + if (!(r1[2] & (1u << 28))) note(buf, len, &count, "AVX"); + if (!(r7[1] & (1u << 5))) note(buf, len, &count, "AVX2"); + if (!(r1[2] & (1u << 12))) note(buf, len, &count, "FMA"); + if (!(r7[1] & (1u << 3))) note(buf, len, &count, "BMI1"); + if (!(r7[1] & (1u << 8))) note(buf, len, &count, "BMI2"); + if (!(rx[2] & (1u << 5))) note(buf, len, &count, "LZCNT"); + if (!(r1[2] & (1u << 22))) note(buf, len, &count, "MOVBE"); + if (!(r1[2] & (1u << 29))) note(buf, len, &count, "F16C"); + if (!os_avx) note(buf, len, &count, "OS-AVX-state"); + return count; +} +#else +int cabledyn_cpu_missing_features(char *buf, int len) { + if (buf && len > 0) buf[0] = '\0'; + return 0; +} +#endif + +#if defined(CABLEDYN_REQUIRE_X86_64_V3) +int cabledyn_cpu_guard_armed(void) { return 1; } + +#if defined(_WIN32) +#include +static int simulated_missing(char *buf, int len) { + DWORD n = GetEnvironmentVariableA("CABLEDYN_CPU_GUARD_SIMULATE", buf, (DWORD)len); + return n > 0 && n < (DWORD)len; +} +static void stop(const char *text, int n) { + DWORD written = 0; + HANDLE err = GetStdHandle(STD_ERROR_HANDLE); + if (err != NULL && err != INVALID_HANDLE_VALUE) WriteFile(err, text, (DWORD)n, &written, NULL); + ExitProcess(3); +} +#else +#include +#include +static int simulated_missing(char *buf, int len) { + const char *value = getenv("CABLEDYN_CPU_GUARD_SIMULATE"); + if (!value || !value[0]) return 0; + buf[0] = '\0'; + return append(buf, len, value, 0); +} +static void stop(const char *text, int n) { + ssize_t ignored = write(STDERR_FILENO, text, (size_t)n); + (void)ignored; + _exit(3); +} +#endif + +static void cabledyn_cpu_guard_check(void) { + char missing[128], message[512]; + int n = 0; + if (!simulated_missing(missing, (int)sizeof missing) && + cabledyn_cpu_missing_features(missing, (int)sizeof missing) == 0) + return; + message[0] = '\0'; + append(message, (int)sizeof message, + "CableDyn: this build needs an x86-64 processor with AVX2 (x86-64-v3: Intel Haswell " + "or AMD Excavator and newer). This computer lacks: ", 0); + append(message, (int)sizeof message, missing, 0); + append(message, (int)sizeof message, + ".\nCableDyn: build CableDyn from source without CABLEDYN_TARGET_AVX2 to run here.\n", 0); + while (message[n]) ++n; + stop(message, n); +} + +#if defined(_MSC_VER) +/* A CRT initializer: runs during C run-time start-up of the executable or DLL, before + * main and before any Fortran code. The object is linked wherever cabledyn_cpu_guard_armed + * is referenced (the driver, the OpenFAST module, the shared library). */ +#pragma section(".CRT$XCU", read) +__declspec(allocate(".CRT$XCU")) void (*cabledyn_cpu_guard_init)(void) = cabledyn_cpu_guard_check; +#else +__attribute__((constructor)) static void cabledyn_cpu_guard_ctor(void) { cabledyn_cpu_guard_check(); } +#endif +#else +int cabledyn_cpu_guard_armed(void) { return 0; } +#endif diff --git a/src/openfast/CableDyn_OF.f90 b/src/openfast/CableDyn_OF.f90 index 439402f..ba710ac 100644 --- a/src/openfast/CableDyn_OF.f90 +++ b/src/openfast/CableDyn_OF.f90 @@ -122,6 +122,15 @@ MODULE CableDyn PRIVATE TYPE(ProgDesc), PARAMETER :: CableDyn_ProgDesc = ProgDesc( 'CableDyn', 'v0.2.0', '2026-10-20' ) + INTERFACE + ! 1 when the CableDyn core targets x86-64-v3 (CABLEDYN_TARGET_AVX2) and carries the + ! start-up processor check of src/cabledyn_cpu_guard.c; the reference also keeps that + ! check in a static link of the core into the OpenFAST executable + FUNCTION cd_cpu_guard_armed() BIND(C, name='cabledyn_cpu_guard_armed') RESULT(armed) + USE, INTRINSIC :: ISO_C_BINDING, ONLY: C_INT + INTEGER(C_INT) :: armed + END FUNCTION cd_cpu_guard_armed + END INTERFACE !> Module-level instance registry: the CableDyn solver objects cannot live inside the !> registry-generated types, so each initialized module holds an integer handle @@ -528,6 +537,7 @@ SUBROUTINE CD_Init(InitInp, u, p, x, xd, z, other, y, m, DTcoupling, InitOut, Er CALL WrScr( ' Author Prof. Jae Hoon Seo, Inha University. License Apache-2.0.' ) CALL WrScr( ' CompMooring = 5: mooring and dynamic power-cable loads are computed by CableDyn,' ) CALL WrScr( ' an OpenFAST mooring module alongside MoorDyn (CompMooring = 3) in this binary.' ) + IF (cd_cpu_guard_armed() /= 0) CALL WrScr( ' CableDyn build: x86-64-v3 (AVX2).' ) ! ------------------------------------------------------------------------------ ! Scope guards (named fail-closed messages; see the module header) diff --git a/tests/test_cpu_guard.f90 b/tests/test_cpu_guard.f90 new file mode 100644 index 0000000..257c77b --- /dev/null +++ b/tests/test_cpu_guard.f90 @@ -0,0 +1,65 @@ +! File: tests/test_cpu_guard.f90 +! SPDX-License-Identifier: Apache-2.0 +! Copyright (c) 2026 Jae Hoon Seo, SMI Lab, Inha University +PROGRAM test_cpu_guard + !! The x86-64-v3 start-up processor check (src/cabledyn_cpu_guard.c). + !! + !! Argument 1 is 1 when the build sets CABLEDYN_TARGET_AVX2 and 0 otherwise: the guard must + !! be armed exactly then. An armed guard has already let this process start, so the processor + !! must then report no missing feature. The feature list and its count must agree, and a + !! short buffer must be truncated, never overrun. + USE, INTRINSIC :: ISO_C_BINDING, ONLY: C_INT, C_CHAR, C_NULL_CHAR + IMPLICIT NONE + INTERFACE + FUNCTION cpu_guard_armed() BIND(C, name='cabledyn_cpu_guard_armed') RESULT(armed) + IMPORT :: C_INT + INTEGER(C_INT) :: armed + END FUNCTION cpu_guard_armed + FUNCTION cpu_missing_features(buf, length) BIND(C, name='cabledyn_cpu_missing_features') RESULT(n) + IMPORT :: C_INT, C_CHAR + CHARACTER(KIND=C_CHAR), INTENT(OUT) :: buf(*) + INTEGER(C_INT), VALUE :: length + INTEGER(C_INT) :: n + END FUNCTION cpu_missing_features + END INTERFACE + CHARACTER(KIND=C_CHAR) :: buf(160), tiny(4) + CHARACTER(16) :: arg + INTEGER :: expected, n, n_tiny, used, i, nfail + + nfail = 0 + CALL GET_COMMAND_ARGUMENT(1, arg) + READ (arg, *) expected + + IF (cpu_guard_armed() /= expected) THEN + PRINT '(A,I0,A,I0)', 'FAIL: guard armed = ', cpu_guard_armed(), ', build expects ', expected + nfail = nfail + 1 + END IF + + buf = 'x' + n = cpu_missing_features(buf, SIZE(buf)) + used = 0 + DO i = 1, SIZE(buf) + IF (buf(i) == C_NULL_CHAR) EXIT + used = i + END DO + PRINT '(A,I0,A,160A)', 'missing x86-64-v3 features: ', n, ' ', buf(1:used) + IF ((n == 0) .NEQV. (used == 0)) THEN + PRINT '(A)', 'FAIL: the feature count and the feature list disagree' + nfail = nfail + 1 + END IF + IF (expected == 1 .AND. n /= 0) THEN + PRINT '(A)', 'FAIL: an armed guard let a processor without x86-64-v3 start' + nfail = nfail + 1 + END IF + + ! a 4-character buffer holds at most 3 characters and the terminator + tiny = 'y' + n_tiny = cpu_missing_features(tiny, SIZE(tiny)) + IF (n_tiny /= n .OR. ALL(tiny /= C_NULL_CHAR)) THEN + PRINT '(A)', 'FAIL: a short buffer changed the count or lost its terminator' + nfail = nfail + 1 + END IF + + IF (nfail > 0) ERROR STOP 1 + PRINT '(A)', 'PASS: cpu guard' +END PROGRAM test_cpu_guard