Skip to content

Commit

Permalink
macOS universal build improvements (#423)
Browse files Browse the repository at this point in the history
This change provides a new implementation of macOS universal binary builds, which is considerably simpler than the previous integration and more useful in as far as it supports the x86_64h slice for AVX2.

Using the ASTCENC_UNIVERSAL_BUILD will now always build the SSE41, AVX2 and NEON single ISA build variants, and use lipo to package them together into a single binary. When in universal mode only the combined binary is installed; the single ISA binaries are built but are treated as internal intermediate build artefacts.

This is the only ISA support provided by the universal build - you can't build with a subset, and you can't use the NONE or SSE2 variants.
  • Loading branch information
solidpixel committed Jun 13, 2023
1 parent 95cc22e commit 190e11e
Show file tree
Hide file tree
Showing 9 changed files with 249 additions and 341 deletions.
130 changes: 25 additions & 105 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS 1)
set(CMAKE_XCODE_ATTRIBUTE_VALID_ARCHS "x86_64 x86_64h arm64")

include(CTest)

Expand All @@ -47,14 +48,29 @@ option(ASTCENC_UNITTEST "Enable astcenc builds with unit tests")
option(ASTCENC_INVARIANCE "Enable astcenc floating point invariance" ON)
option(ASTCENC_CLI "Enable build of astcenc command line tools" ON)

set(ASTCENC_UNIVERSAL_BUILD OFF)
set(ASTCENC_MACOS_BUILD OFF)
set(ASTCENC_MACOS_ARCH_LEN 0)

# Preflight for some macOS-specific build options
if("${CMAKE_SYSTEM_NAME}" STREQUAL "Darwin")
set(ASTCENC_MACOS_BUILD ON)
list(LENGTH CMAKE_OSX_ARCHITECTURES ASTCENC_MACOS_ARCH_LEN)
option(ASTCENC_UNIVERSAL_BUILD "Enable universal multi-arch build" ON)

if(${ASTCENC_UNIVERSAL_BUILD})
set(ASTCENC_ISA_SSE41 ON)
set(ASTCENC_ISA_AVX2 ON)
set(ASTCENC_ISA_NEON ON)

if(${ASTCENC_ISA_SSE2})
message(FATAL_ERROR "ISA_SSE2 cannot be used in a universal build")
endif()

if(${ASTCENC_ISA_NONE})
message(FATAL_ERROR "ISA_NONE cannot be used in a universal build")
endif()

if(${ASTCENC_ISA_NATIVE})
message(FATAL_ERROR "ISA_NATIVE cannot be used in a universal build")
endif()
endif()
else()
set(ASTCENC_UNIVERSAL_BUILD OFF)
endif()

# Count options which MUST be x64
Expand All @@ -75,104 +91,8 @@ foreach(ASTCENC_CONFIG ${ASTCENC_CONFIGS})
endif()
endforeach()

# macOS builds
if("${ASTCENC_MACOS_BUILD}")
list(FIND CMAKE_OSX_ARCHITECTURES "x86_64" ASTCENC_IS_X64)
list(FIND CMAKE_OSX_ARCHITECTURES "arm64" ASTCENC_IS_ARM64)
list(FIND CMAKE_OSX_ARCHITECTURES "$(ARCHS_STANDARD)" ASTCENC_IS_AUTO)

# Turn list index into boolean
if(${ASTCENC_IS_X64} EQUAL -1)
set(ASTCENC_IS_X64 OFF)
else()
set(ASTCENC_IS_X64 ON)
endif()

if(${ASTCENC_IS_ARM64} EQUAL -1)
set(ASTCENC_IS_ARM64 OFF)
else()
set(ASTCENC_IS_ARM64 ON)
endif()

if(${ASTCENC_IS_AUTO} EQUAL -1)
set(ASTCENC_IS_AUTO OFF)
else()
set(ASTCENC_IS_AUTO ON)
endif()

# Set up defaults if no more specific ISA set - use XCode's own defaults
if((ASTCENC_IS_ARM64 OR ASTCENC_IS_AUTO) AND ("${ASTCENC_ARM64_ISA_COUNT}" EQUAL 0) AND (NOT "${ASTCENC_ISA_NONE}"))
set(ASTCENC_ARM64_ISA_COUNT 1)
set(ASTCENC_ISA_NEON ON)
endif()

if((ASTCENC_IS_X64 OR ASTCENC_IS_AUTO) AND ("${ASTCENC_X64_ISA_COUNT}" EQUAL 0) AND (NOT "${ASTCENC_ISA_NONE}"))
set(ASTCENC_X64_ISA_COUNT 1)
set(ASTCENC_ISA_SSE41 ON)
endif()

# User might be doing multi-architecture - XCode sets this at runtime
if("${ASTCENC_IS_AUTO}")
if(("${ASTCENC_ARM64_ISA_COUNT}" GREATER 1) OR ("${ASTCENC_X64_ISA_COUNT}" GREATER 1))
message(FATAL_ERROR "For macOS universal binaries only one backend per architecture is allowed.")
endif()

set(ASTCENC_UNIVERSAL_BUILD ON)

# User requested explicit multi-architecture universal build
elseif("${ASTCENC_MACOS_ARCH_LEN}" GREATER 2)
message(FATAL_ERROR "For macOS universal binaries only x86_64 and arm64 builds are allowed.")

elseif("${ASTCENC_MACOS_ARCH_LEN}" EQUAL 2)
if(NOT (${ASTCENC_IS_X64} AND ${ASTCENC_IS_ARM64}))
message(FATAL_ERROR "For macOS universal binaries only x86_64 and arm64 builds are allowed.")
endif()

if(("${ASTCENC_ARM64_ISA_COUNT}" GREATER 1) OR ("${ASTCENC_X64_ISA_COUNT}" GREATER 1))
message(FATAL_ERROR "For macOS universal binaries only one backend per architecture is allowed.")
endif()

set(ASTCENC_UNIVERSAL_BUILD ON)

# User requested explicit single architecture build
elseif("${ASTCENC_MACOS_ARCH_LEN}" EQUAL 1)
if("${ASTCENC_IS_X64}" AND "${ASTCENC_ARM64_ISA_COUNT}")
message(FATAL_ERROR "For macOS x86_64 builds an arm64 backend cannot be specified.")
endif()

if("${ASTCENC_IS_ARM64}" AND "${ASTCENC_X64_ISA_COUNT}")
message(FATAL_ERROR "For macOS arm64 builds an x86_64 backend cannot be specified.")
endif()

# Else is this a implicit multi-architecture universal build?
elseif(("${ASTCENC_ARM64_ISA_COUNT}" EQUAL 1) AND ("${ASTCENC_X64_ISA_COUNT}" GREATER 1))
string(CONCAT MSG "For macOS setting multiple architecture backends builds a universal binary. "
"For universal binaries only one backend per architecture is allowed.")
message(FATAL_ERROR "${MSG}")

elseif(("${ASTCENC_X64_ISA_COUNT}" EQUAL 1) AND ("${ASTCENC_ARM64_ISA_COUNT}" GREATER 1))
string(CONCAT MSG "For macOS setting multiple architecture backends builds a universal binary. "
"For universal binaries only one backend per architecture is allowed.")
message(FATAL_ERROR "${MSG}")

elseif(("${ASTCENC_ARM64_ISA_COUNT}" EQUAL 1) AND ("${ASTCENC_X64_ISA_COUNT}" EQUAL 1))
set(ASTCENC_UNIVERSAL_BUILD ON)
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64")

# Else is this an implicit single architecture build?
elseif("${ASTCENC_ARM64_ISA_COUNT}" EQUAL 1)
set(CMAKE_OSX_ARCHITECTURES "arm64")

elseif("${ASTCENC_X64_ISA_COUNT}" EQUAL 1)
set(CMAKE_OSX_ARCHITECTURES "x86_64")

else()
# Do nothing here - assume it defaults to host?

endif()

# Non-macOS builds
else()
if(NOT "${CMAKE_SYSTEM_NAME}" STREQUAL "Darwin")
if(("${ASTCENC_ARM64_ISA_COUNT}" GREATER 0) AND ("${ASTCENC_X64_ISA_COUNT}" GREATER 0))
message(FATAL_ERROR "Builds can only support a single architecture per configure.")
endif()
Expand Down Expand Up @@ -201,7 +121,7 @@ printopt("SSE2 backend " ${ASTCENC_ISA_SSE2})
printopt("NEON backend " ${ASTCENC_ISA_NEON})
printopt("NONE backend " ${ASTCENC_ISA_NONE})
printopt("NATIVE backend " ${ASTCENC_ISA_NATIVE})
if("${ASTCENC_MACOS_BUILD}")
if("${CMAKE_SYSTEM_NAME}" STREQUAL "Darwin")
printopt("Universal bin " ${ASTCENC_UNIVERSAL_BUILD})
endif()
printopt("Invariance " ${ASTCENC_INVARIANCE})
Expand All @@ -216,7 +136,7 @@ add_subdirectory(Source)

# Configure package archive
if(ASTCENC_PACKAGE)
if("${ASTCENC_MACOS_BUILD}")
if("${CMAKE_SYSTEM_NAME}" STREQUAL "Darwin")
string(TOLOWER "macOS" ASTCENC_PKG_OS)
else()
string(TOLOWER ${CMAKE_SYSTEM_NAME} ASTCENC_PKG_OS)
Expand Down
69 changes: 51 additions & 18 deletions Docs/Building.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ backends.

## Windows

Builds for Windows are tested with CMake 3.17 and Visual Studio 2019.
Builds for Windows are tested with CMake 3.17, and Visual Studio 2019 or newer.

### Configuring the build

Expand All @@ -25,12 +25,12 @@ cd build

# Configure your build of choice, for example:

# x86-64 using NMake
cmake -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=..\ ^
# x86-64 using a Visual Studio solution
cmake -G "Visual Studio 16 2019" -T ClangCL -DCMAKE_INSTALL_PREFIX=..\ ^
-DASTCENC_ISA_AVX2=ON -DASTCENC_ISA_SSE41=ON -DASTCENC_ISA_SSE2=ON ..

# x86-64 using Visual Studio solution
cmake -G "Visual Studio 16 2019" -T ClangCL -DCMAKE_INSTALL_PREFIX=..\ ^
# x86-64 using NMake
cmake -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=..\ ^
-DASTCENC_ISA_AVX2=ON -DASTCENC_ISA_SSE41=ON -DASTCENC_ISA_SSE2=ON ..
```

Expand All @@ -54,9 +54,10 @@ cd build
nmake install
```

## macOS and Linux
## macOS and Linux using Make

Builds for macOS and Linux are tested with CMake 3.17 and clang++ 9.0.
Builds for macOS and Linux are tested with CMake 3.17, and clang++ 9.0 or
newer.

> Compiling using g++ is supported, but clang++ builds are faster by ~15%.
Expand Down Expand Up @@ -85,23 +86,21 @@ cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=../
-DASTCENC_ISA_AVX2=ON -DASTCENC_ISA_SSE41=ON -DASTCENC_ISA_SSE2=ON ..

# macOS universal binary build
cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=../ \
-DASTCENC_ISA_AVX2=ON -DASTCENC_ISA_NEON=ON ..
cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=../ ..
```

A single CMake configure can build multiple binaries for a single target CPU
architecture, for example building x64 for both SSE2 and AVX2. Each binary name
will include the build variant as a postfix. It is possible to build any set of
the supported SIMD variants by enabling only the ones you require.

For macOS, we additionally support the ability to build a universal binary,
combining one x86 and one arm64 variant into a single output binary. The OS
will select the correct variant to run for the machine being used to run the
built binary. To build a universal binary select a single x86 variant and a
single arm64 variant, and both will be included in a single output binary. It
is not required, but if `CMAKE_OSX_ARCHITECTURES` is set on the command line
(e.g. by XCode-generated build commands) it will be validated against the other
configuration variant settings.
For macOS, we additionally support the ability to build a universal binary.
This build includes SSE4.1 (`x86_64`), AVX2 (`x86_64h`), and NEON (`arm64`)
build slices in a single output binary. The OS will select the correct variant
to run for the machine being used. This is the default build target for a macOS
build, but single-target binaries can still be built by setting
`-DASTCENC_UNIVERSAL_BINARY=OFF` and then manually selecting the specific ISA
variants that are required.

### Building

Expand All @@ -115,6 +114,38 @@ cd build
make install -j16
```

## macOS using XCode

Builds for macOS and Linux are tested with CMake 3.17, and XCode 14.0 or
newer.

### Configuring the build

To use CMake you must first configure the build. Create a build directory
in the root of the astcenc checkout, and then run `cmake` inside that directory
to generate the build system.

```shell
# Create a build directory
mkdir build
cd build

# Configure a universal build
cmake -G Xcode -DCMAKE_INSTALL_PREFIX=../ ..
```

### Building

Once you have configured the build you can use CMake to compile the project
from your build dir, and install to your target install directory.

```shell
cmake --build . --config Release

# Optionally install the binaries to the installation directory
cmake --install . --config Release
```

## Advanced build options

For codec developers and power users there are a number of useful features in
Expand All @@ -136,7 +167,9 @@ which can make profiling more challenging ...
### Shared Libraries

We support building the core library as a shared object by setting the CMake
option `-DASTCENC_SHAREDLIB=ON` at configure time.
option `-DASTCENC_SHAREDLIB=ON` at configure time. For macOS build targets the
shared library supports the same universal build configuration as the command
line utility.

Note that the command line tool is always statically linked; the shared objects
are an extra build output that are not currently used by the command line tool.
Expand Down
6 changes: 5 additions & 1 deletion Docs/ChangeLog-4x.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ The 4.5.0 release is a maintenance release with minor fixes and improvements.
with `/fp:contract`. This should improve performance for MSVC builds.
* **Change:** CMake config variables now use an `ASTCENC_` prefix to add a
namespace and group options when the library is used in a larger project.
* **Change:** CMake config `ASTCENC_UNIVERSAL_BUILD` for building macOS
universal binaries has been improved to include the `x86_64h` slice for
AVX2 builds. Universal builds are now on by default for macOS, and always
include NEON (arm64), SSE4.1 (x86_64), and AVX2 (x86_64h) variants.
* **Change:** CMake config `ASTCENC_NO_INVARIANCE` has been inverted to
remove the negated option, and is now `ASTCENC_INVARIANCE` with a default
of `ON`. Disablign this option can substantially improve performance, but
of `ON`. Disabling this option can substantially improve performance, but
images can different across platforms and compilers.

<!-- ---------------------------------------------------------------------- -->
Expand Down
79 changes: 57 additions & 22 deletions Source/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,35 +27,70 @@ else()
set(ASTCENC_CODEC enc)
endif()

if(${ASTCENC_UNIVERSAL_BUILD})
if(${ASTCENC_ISA_AVX2})
set(ASTCENC_ISA_SIMD "avx2")
elseif(${ASTCENC_ISA_SSE41})
set(ASTCENC_ISA_SIMD "sse4.1")
elseif(${ASTCENC_ISA_SSE2})
set(ASTCENC_ISA_SIMD "sse2")
endif()
include(cmake_core.cmake)
else()
set(ASTCENC_ARTIFACTS native none neon avx2 sse4.1 sse2)
set(ASTCENC_CONFIGS ${ASTCENC_ISA_NATIVE} ${ASTCENC_ISA_NONE} ${ASTCENC_ISA_NEON} ${ASTCENC_ISA_AVX2} ${ASTCENC_ISA_SSE41} ${ASTCENC_ISA_SSE2})
list(LENGTH ASTCENC_ARTIFACTS ASTCENC_ARTIFACTS_LEN)
math(EXPR ASTCENC_ARTIFACTS_LEN "${ASTCENC_ARTIFACTS_LEN} - 1")

foreach(INDEX RANGE ${ASTCENC_ARTIFACTS_LEN})
list(GET ASTCENC_ARTIFACTS ${INDEX} ASTCENC_ARTIFACT)
list(GET ASTCENC_CONFIGS ${INDEX} ASTCENC_CONFIG)
if(${ASTCENC_CONFIG})
set(ASTCENC_ISA_SIMD ${ASTCENC_ARTIFACT})
include(cmake_core.cmake)
set(ASTCENC_ARTIFACTS native none neon avx2 sse4.1 sse2)
set(ASTCENC_CONFIGS ${ASTCENC_ISA_NATIVE} ${ASTCENC_ISA_NONE} ${ASTCENC_ISA_NEON} ${ASTCENC_ISA_AVX2} ${ASTCENC_ISA_SSE41} ${ASTCENC_ISA_SSE2})
list(LENGTH ASTCENC_ARTIFACTS ASTCENC_ARTIFACTS_LEN)
math(EXPR ASTCENC_ARTIFACTS_LEN "${ASTCENC_ARTIFACTS_LEN} - 1")

foreach(INDEX RANGE ${ASTCENC_ARTIFACTS_LEN})
list(GET ASTCENC_ARTIFACTS ${INDEX} ASTCENC_ARTIFACT)
list(GET ASTCENC_CONFIGS ${INDEX} ASTCENC_CONFIG)
if(${ASTCENC_CONFIG})
set(ASTCENC_ISA_SIMD ${ASTCENC_ARTIFACT})

if(${ASTCENC_ISA_SIMD} MATCHES "neon")
set(CMAKE_OSX_ARCHITECTURES arm64)
elseif(${ASTCENC_ISA_SIMD} MATCHES "avx2")
set(CMAKE_OSX_ARCHITECTURES x86_64h)
elseif(NOT ${ASTCENC_ISA_SIMD} MATCHES "none")
set(CMAKE_OSX_ARCHITECTURES x86_64)
endif()
endforeach()

include(cmake_core.cmake)
endif()
endforeach()

if(${ASTCENC_CLI} AND ${ASTCENC_UNIVERSAL_BUILD})
add_custom_target(
astc${ASTCENC_CODEC}
ALL
COMMAND
lipo -create -output $<TARGET_FILE_DIR:astc${ASTCENC_CODEC}-sse4.1>/astc${ASTCENC_CODEC} -arch x86_64 $<TARGET_FILE:astc${ASTCENC_CODEC}-sse4.1> -arch x86_64h $<TARGET_FILE:astc${ASTCENC_CODEC}-avx2> -arch arm64 $<TARGET_FILE:astc${ASTCENC_CODEC}-neon>
VERBATIM)

add_dependencies(
astc${ASTCENC_CODEC}
astc${ASTCENC_CODEC}-sse4.1
astc${ASTCENC_CODEC}-avx2
astc${ASTCENC_CODEC}-neon)

install(PROGRAMS $<TARGET_FILE_DIR:astc${ASTCENC_CODEC}-sse4.1>/astc${ASTCENC_CODEC}
DESTINATION bin)
endif()

if(${ASTCENC_SHAREDLIB} AND ${ASTCENC_UNIVERSAL_BUILD})
add_custom_target(
astc${ASTCENC_CODEC}-shared
ALL
COMMAND
lipo -create -output $<TARGET_FILE_DIR:astc${ASTCENC_CODEC}-sse4.1-shared>/libastc${ASTCENC_CODEC}-shared.dylib -arch x86_64 $<TARGET_FILE:astc${ASTCENC_CODEC}-sse4.1-shared> -arch x86_64h $<TARGET_FILE:astc${ASTCENC_CODEC}-avx2-shared> -arch arm64 $<TARGET_FILE:astc${ASTCENC_CODEC}-neon-shared>
VERBATIM)

add_dependencies(
astc${ASTCENC_CODEC}-shared
astc${ASTCENC_CODEC}-sse4.1-shared
astc${ASTCENC_CODEC}-avx2-shared
astc${ASTCENC_CODEC}-neon-shared)

install(PROGRAMS $<TARGET_FILE_DIR:astc${ASTCENC_CODEC}-sse4.1-shared>/libastc${ASTCENC_CODEC}-shared.dylib
DESTINATION lib)
endif()

# - - - - - - - - - - - - - - - - - -
# Unit testing
if(${ASTCENC_UNITTEST})
set(INSTALL_GTEST OFF CACHE BOOL "" FORCE)
set(CMAKE_OSX_ARCHITECTURES x86_64;arm64)
add_subdirectory(GoogleTest)
enable_testing()
add_subdirectory(UnitTest)
Expand Down

0 comments on commit 190e11e

Please sign in to comment.