blob: 33b19ca56d8317f941030e3bfebb0851f482524e [file] [log] [blame]
// Copyright 2018 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package graphics contains graphics-related utility functions for local tests.
package graphics
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"chromiumos/tast/common/testexec"
"chromiumos/tast/errors"
"chromiumos/tast/testing"
)
const (
// The common path prefix for DEQP executables.
deqpBaseDir = "/usr/local/deqp"
// Path to the USE flags used by ChromiumCommandBuilder.
uiUseFlagsPath = "/etc/ui_use_flags.txt"
// Path to get/set the dirty_writeback_centisecs kernel parameter.
dirtyWritebackCentisecsPath = "/proc/sys/vm/dirty_writeback_centisecs"
)
// APIType identifies a graphics API that can be tested by DEQP.
type APIType int
const (
// UnknownAPI represents an unknown API.
UnknownAPI APIType = iota
// EGL represents Khronos EGL.
EGL
// GLES2 represents OpenGL ES 2.0.
GLES2
// GLES3 represents OpenGL ES 3.0.
GLES3
// GLES31 represents OpenGL ES 3.1. Note that DEQP also includes OpenGL ES
// 3.2 in this category to reduce testing time.
GLES31
// VK represents Vulkan.
VK
)
// Provided for getting readable API names.
func (a APIType) String() string {
switch a {
case EGL:
return "EGL"
case GLES2:
return "GLES2"
case GLES3:
return "GLES3"
case GLES31:
return "GLES31"
case VK:
return "VK"
}
return fmt.Sprintf("UNKNOWN (%d)", a)
}
// parseUIUseFlags parses the configuration file located at path to get the UI
// USE flags: empty lines and lines starting with # are ignored. No end-of-line
// comments should be used. An empty non-nil map is returned if no flags are
// parsed. This is roughly a port of get_ui_use_flags() defined in
// autotest/files/client/bin/utils.py.
func parseUIUseFlags(path string) (map[string]struct{}, error) {
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
lines := strings.Split(string(b), "\n")
flags := make(map[string]struct{})
for _, l := range lines {
l = strings.TrimSpace(l)
if len(l) > 0 && l[0] != '#' {
flags[l] = struct{}{}
}
}
return flags, nil
}
// api returns a string identifying the graphics api, e.g. gl or gles2. This is
// roughly a port of graphics_api() defined in
// autotest/files/client/bin/utils.py.
func api(uiUseFlags map[string]struct{}) string {
if _, ok := uiUseFlags["opengles"]; ok {
return "gles2"
}
return "gl"
}
// extractOpenGLVersion takes the output of the wflinfo command and attempts to
// extract the OpenGL version. An example of the OpenGL version string expected
// in the wflinfo output is:
// OpenGL version string: OpenGL ES 3.2 Mesa 18.1.0-devel (git-131e871385)
func extractOpenGLVersion(ctx context.Context, wflout string) (major,
minor int, err error) {
re := regexp.MustCompile(
`OpenGL version string: OpenGL ES ([0-9]+).([0-9]+)`)
matches := re.FindAllStringSubmatch(wflout, -1)
if len(matches) != 1 {
if dir, ok := testing.ContextOutDir(ctx); ok {
if err := ioutil.WriteFile(filepath.Join(dir, "wflinfo.txt"),
[]byte(wflout), 0644); err != nil {
testing.ContextLog(ctx, "Failed to write wflinfo output: ", err)
}
}
return 0, 0, errors.Errorf("%d OpenGL version strings found in wflinfo output", len(matches))
}
testing.ContextLogf(ctx, "Got %q", matches[0][0])
if major, err = strconv.Atoi(matches[0][1]); err != nil {
return 0, 0, errors.Wrap(err, "could not parse major version")
}
if minor, err = strconv.Atoi(matches[0][2]); err != nil {
return 0, 0, errors.Wrap(err, "could not parse minor version")
}
return major, minor, nil
}
// GLESVersion returns the OpenGL major and minor versions extracted from the
// output of the wflinfo command. This is roughly a port of get_gles_version()
// defined in autotest/files/client/cros/graphics/graphics_utils.py.
func GLESVersion(ctx context.Context) (major, minor int, err error) {
f, err := parseUIUseFlags(uiUseFlagsPath)
if err != nil {
return 0, 0, errors.Wrap(err, "could not get UI USE flags")
}
cmd := testexec.CommandContext(ctx, "wflinfo", "-p", "null", "-a", api(f))
out, err := cmd.Output()
if err != nil {
cmd.DumpLog(ctx)
return 0, 0, errors.Wrap(err, "running the wflinfo command failed")
}
return extractOpenGLVersion(ctx, string(out))
}
// SupportsVulkanForDEQP decides whether the board supports Vulkan for DEQP
// testing. An error is returned if something unexpected happens while deciding.
// This is a port of part of the functionality of GraphicsApiHelper defined in
// autotest/files/client/cros/graphics/graphics_utils.py.
func SupportsVulkanForDEQP(ctx context.Context) (bool, error) {
// First, search for libvulkan.so.
hasVulkan := false
for _, dir := range []string{"/usr/lib", "/usr/lib64", "/usr/local/lib", "/usr/local/lib64"} {
if _, err := os.Stat(filepath.Join(dir, "libvulkan.so")); err == nil {
hasVulkan = true
break
} else if !os.IsNotExist(err) {
return false, errors.Wrap(err, "libvulkan.so search error")
}
}
if !hasVulkan {
testing.ContextLog(ctx, "Could not find libvulkan.so")
return false, nil
}
// Then, search for the DEQP Vulkan testing binary.
p, err := DEQPExecutable(VK)
if err != nil {
return false, errors.Wrapf(err, "could not get the executable for %v", VK)
}
if _, err := os.Stat(p); err == nil {
return true, nil
} else if !os.IsNotExist(err) {
return false, errors.Wrapf(err, "%v search error", p)
}
testing.ContextLogf(ctx, "Found libvulkan.so but not the %v binary", p)
return false, nil
}
// SupportedAPIs returns an array of supported APIs given the OpenGL version and
// whether Vulkan is supported. If no APIs are supported, nil is returned. This
// is a port of part of the functionality of GraphicsApiHelper defined in
// autotest/files/client/cros/graphics/graphics_utils.py.
func SupportedAPIs(glMajor, glMinor int, vulkan bool) []APIType {
var apis []APIType
if glMajor >= 2 {
apis = append(apis, GLES2)
}
if glMajor >= 3 {
apis = append(apis, GLES3)
if glMajor > 3 || glMinor >= 1 {
apis = append(apis, GLES31)
}
}
if vulkan {
apis = append(apis, VK)
}
return apis
}
// DEQPExecutable maps an API identifier to the path of the appropriate DEQP
// executable (or an empty string if the API identifier is not valid). This is a
// port of part of the functionality of GraphicsApiHelper defined in
// autotest/files/client/cros/graphics/graphics_utils.py.
func DEQPExecutable(api APIType) (string, error) {
switch api {
case EGL:
return "", errors.New("cannot run DEQP/EGL on Chrome OS")
case GLES2:
return filepath.Join(deqpBaseDir, "modules/gles2/deqp-gles2"), nil
case GLES3:
return filepath.Join(deqpBaseDir, "modules/gles3/deqp-gles3"), nil
case GLES31:
return filepath.Join(deqpBaseDir, "modules/gles31/deqp-gles31"), nil
case VK:
return filepath.Join(deqpBaseDir, "external/vulkancts/modules/vulkan/deqp-vk"), nil
}
return "", errors.Errorf("unknown graphics API: %s", api)
}
// DEQPEnvironment returns a list of environment variables of the form
// "key=value" that are appropriate for running DEQP binaries. To build it, the
// function starts from the given environment and modifies the LD_LIBRARY_PATH
// to insert /usr/local/lib:/usr/local/lib64 in the front, even if those two
// folders are already in the value. This is a port of part of the functionality
// of the initialization defined in
// autotest/files/client/site_tests/graphics_dEQP/graphics_dEQP.py.
func DEQPEnvironment(env []string) []string {
// Start from a copy of the passed environment.
nenv := make([]string, len(env))
copy(nenv, env)
// Search for the LD_LIBRARY_PATH variable in the environment.
oldld := ""
ldi := -1
for i, s := range nenv {
// Each s is of the form key=value.
kv := strings.Split(s, "=")
if kv[0] == "LD_LIBRARY_PATH" {
ldi = i
oldld = kv[1]
break
}
}
const paths = "/usr/local/lib:/usr/local/lib64"
if ldi != -1 {
// Found the LD_LIBRARY_PATH variable in the environment.
if len(oldld) > 0 {
nenv[ldi] = fmt.Sprintf("LD_LIBRARY_PATH=%s:%s", paths, oldld)
} else {
nenv[ldi] = "LD_LIBRARY_PATH=" + paths
}
} else {
// Did not find the LD_LIBRARY_PATH variable in the environment.
nenv = append(nenv, "LD_LIBRARY_PATH="+paths)
}
return nenv
}
// SetDirtyWritebackDuration flushes pending data to disk and sets the
// dirty_writeback_centisecs kernel parameter to a specified time. If the time
// is negative, it only flushes pending data without changing the kernel
// parameter. This function should be used before starting graphics tests to
// shorten the time between flushes so that logs retain as much information as
// possible in case the system hangs/reboots. Note that the specified time is
// rounded down to the nearest centisecond. This is a port of the
// set_dirty_writeback_centisecs() function in
// autotest/files/client/bin/utils.py.
func SetDirtyWritebackDuration(ctx context.Context, d time.Duration) error {
// Performing a full sync makes it less likely that there are pending writes
// that might defer logging from being written immediately later.
cmd := testexec.CommandContext(ctx, "sync")
err := cmd.Run()
if err != nil {
cmd.DumpLog(ctx)
return errors.Wrap(err, "sync failed")
}
if d >= 0 {
centisecs := d / (time.Second / 100)
if err = ioutil.WriteFile(dirtyWritebackCentisecsPath, []byte(fmt.Sprintf("%d", centisecs)), 0600); err != nil {
return err
}
// Read back the kernel parameter because it is possible that the
// written value was silently discarded (e.g. if the value was too
// large).
actual, err := GetDirtyWritebackDuration()
if err != nil {
return errors.Wrapf(err, "could not read %v", filepath.Base(dirtyWritebackCentisecsPath))
}
expected := centisecs * (time.Second / 100)
if actual != expected {
return errors.Errorf("%v contains %d after writing %d", filepath.Base(dirtyWritebackCentisecsPath), actual, expected)
}
}
return nil
}
// GetDirtyWritebackDuration reads the dirty_writeback_centisecs kernel
// parameter and returns it as a time.Duration (in nanoseconds). Note that it is
// possible for the returned value to be negative. This is a port of the
// get_dirty_writeback_centisecs() function in
// autotest/files/client/bin/utils.py.
func GetDirtyWritebackDuration() (time.Duration, error) {
b, err := ioutil.ReadFile(dirtyWritebackCentisecsPath)
if err != nil {
return -1, err
}
s := strings.TrimSpace(string(b))
if len(s) == 0 {
return -1, errors.Errorf("%v is empty", filepath.Base(dirtyWritebackCentisecsPath))
}
centisecs, err := strconv.ParseInt(s, 10, 32)
if err != nil {
return -1, errors.Wrapf(err, "could not parse %v", filepath.Base(dirtyWritebackCentisecsPath))
}
return time.Duration(centisecs) * (time.Second / 100), nil
}