증상
프로젝트의 빌드 시스템을 Visual Studio 종속에서 CMake로 마이그레이션하며 컴파일러도 이 참에 더 엄격한 clang-cl로 바꾸기로 했다. 그러던 와중 기존 MSVC에서는 잘 동작하던 코드에서 문제가 발생했다.
아래 코드는 카메라를 Panning 시키는 함수로, DirectXMath의 XMVECTOR 타입을 사용한다.
// Camera.cpp
void Camera::Pan(XMINT2 mouseMove)
{
constexpr float panSensitivity = 0.01f;
MoveRight(mouseMove.x * panSensitivity);
XMVECTOR pos = XMLoadFloat3(&m_currPosition);
XMVECTOR rot = XMLoadFloat4(&m_rotation);
XMVECTOR localUp = XMVector3Rotate(XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f), rot);
pos += -localUp * mouseMove.y * panSensitivity; // 이 부분에서 문제 발생
XMStoreFloat3(&m_currPosition, pos);
}
error: cannot convert between scalar type 'int32_t' (aka 'int') and
vector type 'XMVECTOR' (aka '__m128') as implicit conversion would cause truncation
이는 clang-cl과 clang++ 양쪽 모두에서 발생했으며, localUp(XMVECTOR)과 mouseMove.y(int) 간 * 연산자가 예상한 대로 동작하지 않는다는 신호였다.
근본 원인: __m128 정의가 컴파일러마다 다름
XMVECTOR 타입은 _XM_SSE_INTRINSICS_ 매크로가 정의되어있는 경우 __m128 타입에 대한 aliasing이다.
//------------------------------------------------------------------------------
// Vector intrinsic: Four 32 bit floating point components aligned on a 16 byte
// boundary and mapped to hardware vector registers
#if defined(_XM_SSE_INTRINSICS_) && !defined(_XM_NO_INTRINSICS_)
using XMVECTOR = __m128;
#elif defined(_XM_ARM_NEON_INTRINSICS_) && !defined(_XM_NO_INTRINSICS_)
using XMVECTOR = float32x4_t;
#else
using XMVECTOR = __vector4;
#endif
_XM_SSE_INTRINSICS_ 매크로는 Windows의 경우 기본적으로 활성화된다. (ARM 프로세서를 쓰는 경우 등 제외)
자세한 판정 로직
// DirectXMath.h
#if !defined(_XM_ARM_NEON_INTRINSICS_) && !defined(_XM_SSE_INTRINSICS_) && !defined(_XM_NO_INTRINSICS_)
#if (defined(_M_IX86) || defined(_M_X64) || __i386__ || __x86_64__) && !defined(_M_HYBRID_X86_ARM64) && !defined(_M_ARM64EC)
#define _XM_SSE_INTRINSICS_
#elif defined(_M_ARM) || defined(_M_ARM64) || defined(_M_HYBRID_X86_ARM64) || defined(_M_ARM64EC) || __arm__ || __aarch64__
#define _XM_ARM_NEON_INTRINSICS_
#elif !defined(_XM_NO_INTRINSICS_)
#error DirectX Math does not support this target
#endif
#endif // !_XM_ARM_NEON_INTRINSICS_ && !_XM_SSE_INTRINSICS_ && !_XM_NO_INTRINSICS_
이 __m128 타입의 구현은 컴파일러에 따라 다르다.
MSVC
// xmmintrin.h (MSVC에서 제공되는 버전)
typedef union __declspec(intrin_type) __declspec(align(16)) __m128 {
float m128_f32[4];
unsigned __int64 m128_u64[2];
__int8 m128_i8[16];
__int16 m128_i16[8];
__int32 m128_i32[4];
__int64 m128_i64[2];
unsigned __int8 m128_u8[16];
unsigned __int16 m128_u16[8];
unsigned __int32 m128_u32[4];
} __m128;
타입이 union으로, class type이다. 그래서
// DirectXMathVector.inl
inline XMVECTOR XM_CALLCONV operator*
(
FXMVECTOR V,
const float S
) noexcept
{
return XMVectorScale(V, S);
}
이런 식으로 연산자 오버로딩이 정의될 수 있고, XMVECTOR와 int의 곱셈은 문제가 되지 않는다.
clang
// xmmintrin.h (clang에서 제공되는 버전)
typedef float __m128 __attribute__((__vector_size__(16), __aligned__(16)));
반면 clang에서는 class type이 아니라 그 자체로 raw vector를 의미한다. (GCC에서도 동일한 것으로 알고있다)
C++ 규칙상 비멤버 operator는 최소 한 인자가 user-defined type 이어야 하기 때문에, clang에서는 오버로딩 자체가 불가능하다.
DirectXMath의 대응
그래서 DirectXMath는 clang/GCC에서 연산자 오버로딩을 통째로 끈다:
// DirectXMath.h
#if !defined(_XM_NO_XMVECTOR_OVERLOADS_) && (defined(__clang__) || defined(__GNUC__)) && !defined(_XM_NO_INTRINSICS_)
#define _XM_NO_XMVECTOR_OVERLOADS_
#endif
// ...
#ifndef _XM_NO_XMVECTOR_OVERLOADS_
XMVECTOR XM_CALLCONV operator+ (FXMVECTOR V) noexcept;
XMVECTOR XM_CALLCONV operator- (FXMVECTOR V) noexcept;
XMVECTOR& XM_CALLCONV operator+= (XMVECTOR& V1, FXMVECTOR V2) noexcept;
XMVECTOR& XM_CALLCONV operator-= (XMVECTOR& V1, FXMVECTOR V2) noexcept;
XMVECTOR& XM_CALLCONV operator*= (XMVECTOR& V1, FXMVECTOR V2) noexcept;
XMVECTOR& XM_CALLCONV operator/= (XMVECTOR& V1, FXMVECTOR V2) noexcept;
XMVECTOR& operator*= (XMVECTOR& V, float S) noexcept;
XMVECTOR& operator/= (XMVECTOR& V, float S) noexcept;
XMVECTOR XM_CALLCONV operator+ (FXMVECTOR V1, FXMVECTOR V2) noexcept;
XMVECTOR XM_CALLCONV operator- (FXMVECTOR V1, FXMVECTOR V2) noexcept;
XMVECTOR XM_CALLCONV operator* (FXMVECTOR V1, FXMVECTOR V2) noexcept;
XMVECTOR XM_CALLCONV operator/ (FXMVECTOR V1, FXMVECTOR V2) noexcept;
XMVECTOR XM_CALLCONV operator* (FXMVECTOR V, float S) noexcept;
XMVECTOR XM_CALLCONV operator* (float S, FXMVECTOR V) noexcept;
XMVECTOR XM_CALLCONV operator/ (FXMVECTOR V, float S) noexcept;
#endif /* !_XM_NO_XMVECTOR_OVERLOADS_ */
using XMVECTOR = __m128;
즉 operator* → XMVectorScale 경로는 MSVC 전용이고, clang 빌드에는 그런 경로가 존재하지 않는다. clang에서는 operator*를 built-in vector 산술 연산으로만 인식하게 되고, 해당 연산은 XMVECTOR와 float를 명시적으로 요구한다. 이때 int를 전달하게 되면 clang에서 에러를 내뿜는 것.
해결
두 가지 방법이 존재한다. 난 2번 방법을 택했다: 해당 커밋
1
정수 스칼라를 float로 만들어 XMVECTOR * float가 되게 한다.
pos += -localUp * (mouseMove.y * panSensitivity); // 스칼라끼리 먼저 → float
// 또는 static_cast<float>(mouseMove.y)
2
또는 XMVectorScale같은 함수를 명시적으로 사용하면 된다.
pos += XMVectorScale(localUp, -mouseMove.y * panSensitivity);
-mouseMove.y * panSensitivity가 먼저 float 값으로 평가되기 때문에 정상적인 경로로 실행된다.
XMVectorScale내부 동작을 따라가보면_mm_set_ps1과_mm_mul_ps인트린직을 사용하는데,_mm_set_ps1는 하나의 float를 4개 슬롯에 복사하는 동작이며_mm_mul_ps이 곱셈이다._mm_mul_ps은 내부적으로 clang의 빌트인 벡터 산술 연산을 호출한다.
마무리
이 문제를 만나기 전에는 단순히 ‘오버로딩이 정의되어있으면 굳이 긴 함수 이름 타이핑 칠 필요가 없구나’ 라고 생각했었지만, 사실 그건 MSVC에 국한된 이야기였다. 이래서 여러 시도를 해 봐야하나 보다.