/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the Collabora Office project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 */

#include <tools/cpuid.hxx>
#include <cstdint>

namespace cpuid
{
namespace
{
#if defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_AMD64))
#include <intrin.h>
void getCpuId(uint32_t array[4], uint32_t nInfoType)
{
    __cpuid(reinterpret_cast<int*>(array), nInfoType);
}
#elif (defined(__i386__) || defined(__x86_64__))
#include <cpuid.h>
void getCpuId(uint32_t array[4], uint32_t nInfoType)
{
    __cpuid_count(nInfoType, 0, *(array + 0), *(array + 1), *(array + 2), *(array + 3));
}
#else
void getCpuId(uint32_t array[4], uint32_t /*nInfoType*/)
{
    array[0] = array[1] = array[2] = array[3] = 0;
}
#endif

uint32_t getXCR0()
{
    uint32_t xcr0 = 0;
#if defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_AMD64))
    xcr0 = uint32_t(_xgetbv(0));
#elif (defined(__i386__) || defined(__x86_64__))
    __asm__("xgetbv" : "=a"(xcr0) : "c"(0) : "%edx");
#endif
    return xcr0;
}

// For AVX we need to check if OS has support for ymm registers
bool checkAVXSupportInOS()
{
    uint32_t xcr0 = getXCR0();
    return ((xcr0 & 0x06) == 0x06); // bits 1,2 = xmm + ymm
}

// For AVX512 we additionally need opmask, ZMM_Hi256, Hi16_ZMM state
bool checkAVX512SupportInOS()
{
    uint32_t xcr0 = getXCR0();
    return ((xcr0 & 0xE6) == 0xE6); // bits 1,2,5,6,7 = xmm + ymm + opmask + ZMM_Hi256 + Hi16_ZMM
}

} // end anonymous namespace

#define HYPER_bit (1 << 28)
#define SSE2_bit (1 << 26)
#define SSSE3_bit (1 << 9)
#define SSE41_bit (1 << 19)
#define SSE42_bit (1 << 20)
#define XSAVE_bit (1 << 27)
#define AVX_bit (1 << 28)
#define AVX2_bit (1 << 5)
#define AVX512F_bit (1 << 16)

InstructionSetFlags getCpuInstructionSetFlags()
{
    InstructionSetFlags eInstructions = InstructionSetFlags::NONE;

    uint32_t info[] = { 0, 0, 0, 0 };
    getCpuId(info, 0);
    int nLevel = info[0];

    if (nLevel >= 1)
    {
        uint32_t aCpuInfoArray[] = { 0, 0, 0, 0 };
        getCpuId(aCpuInfoArray, 1);

        if ((aCpuInfoArray[3] & HYPER_bit) != 0)
            eInstructions |= InstructionSetFlags::HYPER;

        if ((aCpuInfoArray[3] & SSE2_bit) != 0)
            eInstructions |= InstructionSetFlags::SSE2;

        if ((aCpuInfoArray[2] & SSSE3_bit) != 0)
            eInstructions |= InstructionSetFlags::SSSE3;

        if ((aCpuInfoArray[2] & SSE41_bit) != 0)
            eInstructions |= InstructionSetFlags::SSE41;

        if ((aCpuInfoArray[2] & SSE42_bit) != 0)
            eInstructions |= InstructionSetFlags::SSE42;

        if (((aCpuInfoArray[2] & AVX_bit) != 0) && ((aCpuInfoArray[2] & XSAVE_bit) != 0))
        {
            if (checkAVXSupportInOS())
            {
                eInstructions |= InstructionSetFlags::AVX;

                if (nLevel >= 7)
                {
                    uint32_t aExtendedInfo[] = { 0, 0, 0, 0 };
                    getCpuId(aExtendedInfo, 7);

                    if ((aExtendedInfo[1] & AVX2_bit) != 0)
                        eInstructions |= InstructionSetFlags::AVX2;
                    if (((aExtendedInfo[1] & AVX512F_bit) != 0) && checkAVX512SupportInOS())
                        eInstructions |= InstructionSetFlags::AVX512F;
                }
            }
        }
    }

    return eInstructions;
}

bool isCpuInstructionSetSupported(InstructionSetFlags eInstructions)
{
    static InstructionSetFlags eCPUFlags = getCpuInstructionSetFlags();
    return (eCPUFlags & eInstructions) == eInstructions;
}

OUString instructionSetSupportedString()
{
    OUString aString;
    if (isCpuInstructionSetSupported(InstructionSetFlags::SSE2))
        aString += "SSE2 ";
    if (isCpuInstructionSetSupported(InstructionSetFlags::SSSE3))
        aString += "SSSE3 ";
    if (isCpuInstructionSetSupported(InstructionSetFlags::SSE41))
        aString += "SSE4.1 ";
    if (isCpuInstructionSetSupported(InstructionSetFlags::SSE42))
        aString += "SSE4.2 ";
    if (isCpuInstructionSetSupported(InstructionSetFlags::AVX))
        aString += "AVX ";
    if (isCpuInstructionSetSupported(InstructionSetFlags::AVX2))
        aString += "AVX2 ";
    if (isCpuInstructionSetSupported(InstructionSetFlags::AVX512F))
        aString += "AVX512F ";
    return aString;
}

} // end cpuid

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
