blob: 70b5af386e956bec7fac34aad95712245c8940d6 [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright 2021 Google LLC
#
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# GLSL ES2 conformance test files can be found at
# https://github.com/KhronosGroup/VK-GL-CTS/tree/master/data/gles2/shaders
#
# Usage:
# cat ${TEST_FILES}/*.test | ./import_conformance_tests.py
#
# This will generate two directories, "pass" and "fail", containing finished runtime shaders.
# Note that some tests were originally designed to fail, because a conforming compiler should not
# allow the program. A handful of others fail because they are incompatible with SkSL. This script
# identifies SkSL-incompatible tests them and moves them from "pass" to "fail" automatically.
#
# Not all ES2 test files are meaningful in SkSL. These input files are not supported:
# - linkage.test: Runtime Effects only handle fragment processing
# - invalid_texture_functions.test: no GL texture functions in Runtime Effects
# - preprocessor.test: no preprocessor in SkSL
import os
import pyparsing as pp
import re
import sys
# Each case can contain expected input/output values, sometimes in [bracketed|lists] and
# sometimes not.
# GLSL code appears in ""double-double quotes"" and is indicated as vert/frag-specific or "both".
# Some tests denote that they are expected to pass/fail. (Presumably, "pass" is the default.)
# We ignore descriptions and version fields.
wordWithUnderscores = pp.Word(pp.alphanums + '_')
pipeList = pp.delimited_list(pp.SkipTo(pp.Literal("|") | pp.Literal("]")), delim="|")
bracketedPipeList = pp.Group(pp.Literal("[").suppress() +
pipeList +
pp.Literal("]").suppress())
unbracketedValue = pp.Group(pp.SkipTo(";"))
valueList = (pp.Word(pp.alphanums) + # type
pp.Word(pp.alphanums) + # varname
pp.Literal("=").suppress() +
(bracketedPipeList | unbracketedValue) +
pp.Literal(";").suppress())
value = pp.Group((pp.Keyword("input") | pp.Keyword("output") | pp.Keyword("uniform")) +
valueList)
values = (pp.Keyword("values") +
pp.Literal("{").suppress() +
pp.ZeroOrMore(value) +
pp.Literal("}").suppress())
expectation = (pp.Keyword("expect").suppress() + (pp.Keyword("compile_fail") |
pp.Keyword("pass")))
code = ((pp.Keyword("both") + pp.QuotedString('""', multiline=True)) |
(pp.Keyword("vertex") + pp.QuotedString('""', multiline=True) +
pp.Keyword("fragment") + pp.QuotedString('""', multiline=True)))
reqGlsl100 = pp.Keyword("require").suppress() + pp.Keyword("full_glsl_es_100_support")
desc = pp.Keyword("desc") + pp.QuotedString('"')
version100es = pp.Keyword("version") + pp.Keyword("100") + pp.Keyword("es")
ignoredCaseItem = (desc | version100es).suppress()
caseItem = pp.Group(values | expectation | code | reqGlsl100) | ignoredCaseItem
caseBody = pp.ZeroOrMore(caseItem)
blockEnd = pp.Keyword("end").suppress();
caseHeader = pp.Keyword("case") + wordWithUnderscores
case = pp.Group(caseHeader + caseBody + blockEnd)
# Groups can be nested to any depth (or can be absent), and may contain any number of cases.
# The names in the group header are ignored.
groupHeader = (pp.Keyword("group") + wordWithUnderscores + pp.QuotedString('"')).suppress()
group = pp.Forward()
group <<= pp.OneOrMore(case | (groupHeader + group + blockEnd))
# The full grammar is just the group specification, plus the fact that # indicates a comment.
grammar = group
group.ignore('#' + pp.restOfLine)
testCases = grammar.parse_string(sys.stdin.read(), parse_all=True)
# Write output files in subdirectories next to this script.
testDirectory = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
passDirectory = testDirectory + "/pass"
failDirectory = testDirectory + "/fail"
os.makedirs(passDirectory, exist_ok=True)
os.makedirs(failDirectory, exist_ok=True)
written = {}
for c in testCases:
# Parse the case header
assert c[0] == 'case'
c.pop(0)
testName = c[0]
assert isinstance(testName, str)
c.pop(0)
# Parse the case body
skipTest = ''
expectPass = True
allowMismatch = False
testCode = ''
inputs = []
outputs = []
for b in c:
caseItem = b[0]
b.pop(0)
if caseItem == 'compile_fail':
expectPass = False
elif caseItem == 'pass':
expectPass = True
elif caseItem == 'vertex' or caseItem == 'fragment':
skipTest = 'Uses vertex'
elif caseItem == 'both':
testCode = b[0]
assert isinstance(testCode, str)
elif caseItem == 'values':
for v in b:
valueType = v[0]
v.pop(0)
if valueType == 'uniform':
skipTest = 'Uses uniform'
elif valueType == 'input':
inputs.append(v.asList())
elif valueType == 'output':
outputs.append(v.asList())
elif caseItem == 'full_glsl_es_100_support':
skipTest = 'Uses while loop'
else:
assert 0
if skipTest == '':
if "void main" not in testCode:
skipTest = 'Missing main'
if skipTest != '':
print("skipped %s (%s)" % (testName, skipTest))
continue
# The test is safe to run, but it might not get the same result.
# SkSL does not guarantee that function arguments will always be evaluated left-to-right.
if re.fullmatch('argument_eval_order_[12]', testName):
allowMismatch = True
print("allowing mismatch in %s" % testName)
# The ES2 conformance tests allow floating point comparisons to be off by 0.05:
# https://osscs.corp.google.com/android/platform/superproject/+/master:external/deqp/external/openglcts/modules/common/glcShaderLibraryCase.cpp;l=714;drc=84322c9402f810da3cd80b52e9f9ef72150a9004
# A few tests require this slop factor to pass, regardless of GPU precision
# (e.g. the test is written to expect 2.19, but actually computes 2.194285)
compare = lambda type, a, b : '((' + a + ') == (' + b + '))'
if (testName == 'math_float' or
testName == 'struct' or
testName == 'nested_struct' or
testName == 'nested_builtin_funcs'):
compare = lambda type, a, b : (
'(floor(20 * abs((' + a + ') - (' + b + '))) == ' + type + '(0))'
)
# Switch tests to a "fail" expectation instead of "pass" when SkSL and GLSL disagree.
# SkSL does not support casts which discard elements such as `float(myFloat4)`.
if (re.fullmatch('(vec|bvec|ivec)[234]_to_(float|int|bool)', testName) or
re.fullmatch('(vec|bvec|ivec)[34]_to_(vec|bvec|ivec)2', testName) or
re.fullmatch('(vec|bvec|ivec)[4]_to_(vec|bvec|ivec)3', testName) or
# SkSL requires that function out-parameters match the precision of the variable passed in.
re.fullmatch('(out|inout)_lowp_(int|float)', testName) or
# SkSL rejects code that fails to return a value; GLSL ES2 allows it.
testName == 'missing_returns' or
# SkSL does not support a global `precision` directive.
testName == 'default_vs_explicit_precision' or
# SkSL does not allow variables to be created without an enclosing scope.
testName == 'variable_in_if_hides_global_variable'):
assert expectPass
expectPass = False
print("moved %s to fail" % testName)
# Apply fixups to the test code.
# SkSL doesn't support the `precision` keyword, so comment it out if it appears.
testCode = testCode.replace("precision highp ", "// precision highp ");
testCode = testCode.replace("precision mediump ", "// precision mediump ");
testCode = testCode.replace("precision lowp ", "// precision lowp ");
# SkSL doesn't support the `#version` declaration.
testCode = testCode.replace("#version", "// #version");
# Rename the `main` function to `execute_test`.
testCode = testCode.replace("void main", "bool execute_test");
# Replace ${POSITION_FRAG_COLOR} with a scratch variable.
if "${POSITION_FRAG_COLOR}" in testCode:
testCode = testCode.replace("${POSITION_FRAG_COLOR}", "PositionFragColor");
if "${DECLARATIONS}" in testCode:
testCode = testCode.replace("${DECLARATIONS}",
"vec4 PositionFragColor;\n${DECLARATIONS}");
else:
testCode = "vec4 PositionFragColor;\n" + testCode
# Create a runnable SkSL test by returning green or red based on the test result.
testCode += "\n"
testCode += "half4 main(float2 coords) {\n"
testCode += " return execute_test() ? half4(0,1,0,1) : half4(1,0,0,1);\n"
testCode += "}\n"
testDirectory = passDirectory
if not expectPass:
testDirectory = failDirectory
# Find the total number of input/output fields.
numVariables = 0
for v in inputs + outputs:
numVariables = max(numVariables, len(v[2]))
if numVariables > 0:
assert "${DECLARATIONS}" in testCode
assert "${OUTPUT}" in testCode
for varIndex in range(0, numVariables):
testSpecialization = testCode
# Assemble input variable declarations for ${DECLARATIONS}.
declarations = ""
for v in inputs:
if len(v[2]) > varIndex:
declarations += "%s %s = %s;\n" % (v[0], v[1], v[2][varIndex]);
# Assemble output variable declarations for ${DECLARATIONS}.
for v in outputs:
declarations += "%s %s;\n" % (v[0], v[1]);
# Verify output values inside ${OUTPUT}.
outputChecks = "return true"
if not allowMismatch:
for v in outputs:
if len(v[2]) > varIndex:
outputChecks += " && " + compare(v[0], v[1], v[2][varIndex])
outputChecks += ";\n"
# Apply fixups to the test code.
testSpecialization = testSpecialization.replace("${DECLARATIONS}", declarations)
testSpecialization = testSpecialization.replace("${SETUP}", '')
testSpecialization = testSpecialization.replace("${OUTPUT}", outputChecks)
# Generate an SkSL test file.
path = "%s/%s_%d.rts" % (testDirectory, testName, varIndex)
assert path not in written
written[path] = True
f = open(path, "w")
f.write(testSpecialization)
else: # not (numVariables > 0)
testCode = testCode.replace("${DECLARATIONS}", '')
testCode = testCode.replace("${SETUP}", '')
testCode = testCode.replace("${OUTPUT}", 'return true;')
# Generate an SkSL test file.
path = "%s/%s.rts" % (testDirectory, testName)
assert path not in written
written[path] = True
f = open(path, "w")
f.write(testCode)