#!/usr/bin/python3
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
# A script to generate a CSV file showing the x86_64 ABI
# compatibility levels for each CPU model.
#

from qemu import qmp
import sys

if len(sys.argv) != 1:
    print("syntax: %s QMP-SOCK\n\n" % __file__ +
          "Where QMP-SOCK points to a QEMU process such as\n\n" +
          " # qemu-system-x86_64 -qmp unix:/tmp/qmp,server,nowait " +
          "-display none -accel kvm", file=sys.stderr)
    sys.exit(1)

# Mandatory CPUID features for each microarch ABI level
levels = [
    [ # x86-64 baseline
        "cmov",
        "cx8",
        "fpu",
        "fxsr",
        "mmx",
        "syscall",
        "sse",
        "sse2",
    ],
    [ # x86-64-v2
        "cx16",
        "lahf-lm",
        "popcnt",
        "pni",
        "sse4.1",
        "sse4.2",
        "ssse3",
    ],
    [ # x86-64-v3
        "avx",
        "avx2",
        "bmi1",
        "bmi2",
        "f16c",
        "fma",
        "abm",
        "movbe",
    ],
    [ # x86-64-v4
        "avx512f",
        "avx512bw",
        "avx512cd",
        "avx512dq",
        "avx512vl",
    ],
]

# Assumes externally launched process such as
#
#   qemu-system-x86_64 -qmp unix:/tmp/qmp,server,nowait -display none -accel kvm
#
# Note different results will be obtained with TCG, as
# TCG masks out certain features otherwise present in
# the CPU model definitions, as does KVM.


sock = sys.argv[1]
cmd = sys.argv[2]
shell = qmp.QEMUMonitorProtocol(sock)
shell.connect()

models = shell.cmd("query-cpu-definitions")

# These QMP props don't correspond to CPUID fatures
# so ignore them
skip = [
    "family",
    "min-level",
    "min-xlevel",
    "vendor",
    "model",
    "model-id",
    "stepping",
]

names = []

for model in models["return"]:
    if "alias-of" in model:
        continue
    names.append(model["name"])

models = {}

for name in sorted(names):
    cpu = shell.cmd("query-cpu-model-expansion",
                     { "type": "static",
                       "model": { "name": name }})

    got = {}
    for (feature, present) in cpu["return"]["model"]["props"].items():
        if present and feature not in skip:
            got[feature] = True

    if name in ["host", "max", "base"]:
        continue

    models[name] = {
        # Dict of all present features in this CPU model
        "features": got,

        # Whether each x86-64 ABI level is satisfied
        "levels": [False, False, False, False],

        # Number of extra CPUID features compared to the x86-64 ABI level
        "distance":[-1, -1, -1, -1],

        # CPUID features present in model, but not in ABI level
        "delta":[[], [], [], []],

        # CPUID features in ABI level but not present in model
        "missing": [[], [], [], []],
    }


# Calculate whether the CPU models satisfy each ABI level
for name in models.keys():
    for level in range(len(levels)):
        got = set(models[name]["features"])
        want = set(levels[level])
        missing = want - got
        match = True
        if len(missing) > 0:
            match = False
        models[name]["levels"][level] = match
        models[name]["missing"][level] = missing

# Cache list of CPU models satisfying each ABI level
abi_models = [
    [],
    [],
    [],
    [],
]

for name in models.keys():
    for level in range(len(levels)):
        if models[name]["levels"][level]:
            abi_models[level].append(name)


for level in range(len(abi_models)):
    # Find the union of features in all CPU models satisfying this ABI
    allfeatures = {}
    for name in abi_models[level]:
        for feat in models[name]["features"]:
            allfeatures[feat] = True

    # Find the intersection of features in all CPU models satisfying this ABI
    commonfeatures = []
    for feat in allfeatures:
        present = True
        for name in models.keys():
            if not models[name]["levels"][level]:
                continue
            if feat not in models[name]["features"]:
                present = False
        if present:
            commonfeatures.append(feat)

    # Determine how many extra features are present compared to the lowest
    # common denominator
    for name in models.keys():
        if not models[name]["levels"][level]:
            continue

        delta = set(models[name]["features"].keys()) - set(commonfeatures)
        models[name]["distance"][level] = len(delta)
        models[name]["delta"][level] = delta

def print_uarch_abi_csv():
    print("# Automatically generated from '%s'" % __file__)
    print("Model,baseline,v2,v3,v4")
    for name in models.keys():
        print(name, end="")
        for level in range(len(levels)):
            if models[name]["levels"][level]:
                print(",✅", end="")
            else:
                print(",", end="")
        print()

print_uarch_abi_csv()
