From a0468578b67217755a504d2b293920ce6c39f800 Mon Sep 17 00:00:00 2001 From: AWoloszyn Date: Mon, 29 Apr 2019 09:25:33 -0400 Subject: [PATCH] Add Stadia Support. --- BUILD.bazel | 8 + cmd/gapis/BUILD.bazel | 1 + cmd/gapis/main.go | 23 + core/app/layout/layout.go | 27 +- core/codegen/triple.go | 2 + core/net/useragent.go | 2 + core/os/device/abi.go | 1 + core/os/device/device.go | 5 + core/os/device/device.proto | 1 + core/os/device/ggp/BUILD.bazel | 35 + core/os/device/ggp/device.go | 280 ++++++++ core/os/device/ggp/doc.go | 17 + core/os/device/ggp/parse.go | 137 ++++ core/os/device/remotessh/commands.go | 7 +- core/os/shell/command.go | 6 +- core/vulkan/cc/include/vulkan/vulkan.h | 25 + .../vk_virtual_swapchain/cc/BUILD.bazel | 10 +- core/vulkan/vk_virtual_swapchain/cc/layer.cpp | 5 + core/vulkan/vk_virtual_swapchain/cc/layer.h | 3 + .../vk_virtual_swapchain/cc/platform.cpp | 15 + gapir/cc/surface.cpp | 4 + gapir/cc/surface.h | 7 +- .../api/templates/vulkan_gfx_api_extras.tmpl | 116 ++-- gapis/api/vulkan/BUILD.bazel | 1 + gapis/api/vulkan/api/enums.api | 5 + gapis/api/vulkan/extensions/khr_surface.api | 2 + gapis/api/vulkan/ggp/vulkan_ggp.api | 56 ++ gapis/api/vulkan/replay.go | 4 + gapis/api/vulkan/state_rebuilder.go | 13 + .../vulkan/templates/vk_spy_helpers.cpp.tmpl | 5 +- gapis/api/vulkan/vulkan.api | 5 + gapis/trace/desktop/BUILD.bazel | 11 +- gapis/trace/desktop/ggp_trace.go | 600 ++++++++++++++++++ gapis/trace/manager.go | 7 + 34 files changed, 1368 insertions(+), 78 deletions(-) create mode 100644 core/os/device/ggp/BUILD.bazel create mode 100644 core/os/device/ggp/device.go create mode 100644 core/os/device/ggp/doc.go create mode 100644 core/os/device/ggp/parse.go create mode 100644 gapis/api/vulkan/ggp/vulkan_ggp.api create mode 100644 gapis/trace/desktop/ggp_trace.go diff --git a/BUILD.bazel b/BUILD.bazel index e065ebb8e0..0ddc9e7fb3 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -12,6 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +# Description: +# +# Gapid is a graphics API debugger. + +licenses(["notice"]) # Apache 2.0 + +exports_files(["LICENSE"]) + load("@bazel_gazelle//:def.bzl", "gazelle") load("//tools/build:rules.bzl", "copy_to") diff --git a/cmd/gapis/BUILD.bazel b/cmd/gapis/BUILD.bazel index aa3f4fabe9..84f0195f9e 100644 --- a/cmd/gapis/BUILD.bazel +++ b/cmd/gapis/BUILD.bazel @@ -28,6 +28,7 @@ go_library( "//core/log:go_default_library", "//core/os/android/adb:go_default_library", "//core/os/device/bind:go_default_library", + "//core/os/device/ggp:go_default_library", "//core/os/device/host:go_default_library", "//core/os/device/remotessh:go_default_library", "//core/os/file:go_default_library", diff --git a/cmd/gapis/main.go b/cmd/gapis/main.go index 321786a2eb..d16cacc229 100644 --- a/cmd/gapis/main.go +++ b/cmd/gapis/main.go @@ -32,6 +32,7 @@ import ( "github.com/google/gapid/core/log" "github.com/google/gapid/core/os/android/adb" "github.com/google/gapid/core/os/device/bind" + "github.com/google/gapid/core/os/device/ggp" "github.com/google/gapid/core/os/device/host" "github.com/google/gapid/core/os/device/remotessh" "github.com/google/gapid/core/os/file" @@ -128,6 +129,9 @@ func run(ctx context.Context) error { crash.Go(func() { monitorRemoteSSHDevices(ctx, r, wg.Done) }) } + wg.Add(1) + crash.Go(func() { monitorGGPDevices(ctx, r, wg.Done) }) + deviceScanDone, onDeviceScanDone := task.NewSignal() crash.Go(func() { wg.Wait() @@ -201,6 +205,25 @@ func monitorRemoteSSHDevices(ctx context.Context, r *bind.Registry, scanDone fun } } +func monitorGGPDevices(ctx context.Context, r *bind.Registry, scanDone func()) { + + func() { + // Populate the registry with all the existing devices. + defer scanDone() // Signal that we have a primed registry. + + if devs, err := ggp.Devices(ctx); err == nil { + for _, d := range devs { + r.AddDevice(ctx, d) + r.SetDeviceProperty(ctx, d, client.LaunchArgsKey, text.SplitArgs(*gapirArgStr)) + } + } + }() + + if err := ggp.Monitor(ctx, r, time.Second*15); err != nil { + log.W(ctx, "Could not scan for remote GGP devices. Error: %v", err) + } +} + func loadStrings(ctx context.Context) []*stringtable.StringTable { files, err := filepath.Glob(filepath.Join(*stringsPath, "*.stb")) if err != nil { diff --git a/core/app/layout/layout.go b/core/app/layout/layout.go index 71d1012894..9b68ea457f 100644 --- a/core/app/layout/layout.go +++ b/core/app/layout/layout.go @@ -119,17 +119,24 @@ func (l pkgLayout) Gapit(ctx context.Context) (file.Path, error) { return l.root.Join(withExecutablePlatformSuffix("gapit", hostOS(ctx))), nil } -var osToDir = map[device.OSKind]string{ - device.Linux: "linux", - device.OSX: "macos", - device.Windows: "windows", +func osToDir(k device.OSKind) string { + if device.IsLinuxLike(k) { + return "linux" + } + if k == device.OSX { + return "macos" + } + if k == device.Windows { + return "windows" + } + return "" } func (l pkgLayout) Gapir(ctx context.Context, abi *device.ABI) (file.Path, error) { if abi == nil || hostOS(ctx) == abi.OS { return l.root.Join(withExecutablePlatformSuffix("gapir", hostOS(ctx))), nil } - return l.root.Join(osToDir[abi.OS], withExecutablePlatformSuffix("gapir", abi.OS)), nil + return l.root.Join(osToDir(abi.OS), withExecutablePlatformSuffix("gapir", abi.OS)), nil } func (l pkgLayout) Gapis(ctx context.Context) (file.Path, error) { @@ -144,7 +151,7 @@ func (l pkgLayout) Library(ctx context.Context, lib LibraryType, abi *device.ABI if abi == nil || hostOS(ctx) == abi.OS { return l.root.Join("lib", withLibraryPlatformSuffix(libTypeToName[lib], hostOS(ctx))), nil } - return l.root.Join(osToDir[abi.OS], "lib", withLibraryPlatformSuffix(libTypeToName[lib], abi.OS)), nil + return l.root.Join(osToDir(abi.OS), "lib", withLibraryPlatformSuffix(libTypeToName[lib], abi.OS)), nil } func (l pkgLayout) Json(ctx context.Context, lib LibraryType) (file.Path, error) { @@ -159,7 +166,7 @@ func (l pkgLayout) DeviceInfo(ctx context.Context, os device.OSKind) (file.Path, if hostOS(ctx) == os { return l.root.Join(withExecutablePlatformSuffix("device-info", os)), nil } - return l.root.Join(osToDir[os], withExecutablePlatformSuffix("device-info", os)), nil + return l.root.Join(osToDir(os), withExecutablePlatformSuffix("device-info", os)), nil } // NewPkgLayout returns a FileLayout rooted at the given directory with the standard package layout. @@ -364,7 +371,7 @@ func (l *ZipLayout) Gapir(ctx context.Context, abi *device.ABI) (*zip.File, erro if abi == nil || l.os == abi.OS { return l.file(withExecutablePlatformSuffix("gapir", l.os)) } - return l.file(osToDir[abi.OS] + "/" + withExecutablePlatformSuffix("gapir", abi.OS)) + return l.file(osToDir(abi.OS) + "/" + withExecutablePlatformSuffix("gapir", abi.OS)) } // Gapis returns the path to the gapis binary in this layout. @@ -382,7 +389,7 @@ func (l *ZipLayout) Library(ctx context.Context, lib LibraryType, abi *device.AB if abi == nil || l.os == abi.OS { return l.file("lib/" + withLibraryPlatformSuffix(libTypeToName[lib], l.os)) } - return l.file(osToDir[abi.OS] + "lib/" + withLibraryPlatformSuffix(libTypeToName[lib], abi.OS)) + return l.file(osToDir(abi.OS) + "lib/" + withLibraryPlatformSuffix(libTypeToName[lib], abi.OS)) } // Json returns the path to the Vulkan layer JSON definition for the given library. @@ -395,5 +402,5 @@ func (l *ZipLayout) DeviceInfo(ctx context.Context, os device.OSKind) (*zip.File if l.os == os { return l.file(withExecutablePlatformSuffix("device-info", os)) } - return l.file(osToDir[os] + "/" + withExecutablePlatformSuffix("device-info", os)) + return l.file(osToDir(os) + "/" + withExecutablePlatformSuffix("device-info", os)) } diff --git a/core/codegen/triple.go b/core/codegen/triple.go index 48a68e793f..e7237261c8 100644 --- a/core/codegen/triple.go +++ b/core/codegen/triple.go @@ -52,6 +52,8 @@ func TargetTriple(dev *device.ABI) Triple { out.vendor, out.os = "apple", "darwin" case device.Linux: out.os = "linux" + case device.Stadia: + out.os = "linux" case device.Android: out.os, out.abi = "linux", "androideabi" } diff --git a/core/net/useragent.go b/core/net/useragent.go index da7e326cd0..0e131fcbe3 100644 --- a/core/net/useragent.go +++ b/core/net/useragent.go @@ -46,6 +46,8 @@ func UserAgent(d *device.Configuration, ai ApplicationInfo) string { case device.Linux: info = append(info, "Linux") + case device.Stadia: + info = append(info, "Stadia") case device.Android: info = append(info, "Linux", "U", fmt.Sprintf("Android %v.%v.%v", os.MajorVersion, os.MinorVersion, os.PointVersion)) diff --git a/core/os/device/abi.go b/core/os/device/abi.go index 96a190b6aa..79c0f81449 100644 --- a/core/os/device/abi.go +++ b/core/os/device/abi.go @@ -28,6 +28,7 @@ var ( LinuxX86_64 = abi("linux_x64", Linux, X86_64, Little64) OSXX86_64 = abi("osx_x64", OSX, X86_64, Little64) WindowsX86_64 = abi("windows_x64", Windows, X86_64, Little64) + StadiaX86_64 = abi("stadia", Stadia, X86_64, Little64) ) var abiByName = map[string]*ABI{} diff --git a/core/os/device/device.go b/core/os/device/device.go index b8824d6d92..24ff3b6489 100644 --- a/core/os/device/device.go +++ b/core/os/device/device.go @@ -40,8 +40,13 @@ const ( OSX = OSKind_OSX Linux = OSKind_Linux Android = OSKind_Android + Stadia = OSKind_Stadia ) +func IsLinuxLike(k OSKind) bool { + return k == OSKind_Linux || k == OSKind_Stadia +} + var ( // ARMv7aLayout is the memory layout for the armv7a ABI. // http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042f/IHI0042F_aapcs.pdf diff --git a/core/os/device/device.proto b/core/os/device/device.proto index e740ac9b27..a1d77eec1a 100644 --- a/core/os/device/device.proto +++ b/core/os/device/device.proto @@ -51,6 +51,7 @@ enum OSKind { OSX = 2; Linux = 3; Android = 4; + Stadia = 5; } // MemoryLayout holds information about how memory is fundamentally laid out for diff --git a/core/os/device/ggp/BUILD.bazel b/core/os/device/ggp/BUILD.bazel new file mode 100644 index 0000000000..8af0bf3bea --- /dev/null +++ b/core/os/device/ggp/BUILD.bazel @@ -0,0 +1,35 @@ +# Copyright (C) 2019 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "device.go", + "doc.go", + "parse.go", + ], + importpath = "github.com/google/gapid/core/os/device/ggp", + visibility = ["//visibility:public"], + deps = [ + "//core/event/task:go_default_library", + "//core/log:go_default_library", + "//core/os/device:go_default_library", + "//core/os/device/bind:go_default_library", + "//core/os/device/remotessh:go_default_library", + "//core/os/file:go_default_library", + "//core/os/shell:go_default_library", + ], +) diff --git a/core/os/device/ggp/device.go b/core/os/device/ggp/device.go new file mode 100644 index 0000000000..7a6f424f2b --- /dev/null +++ b/core/os/device/ggp/device.go @@ -0,0 +1,280 @@ +// Copyright (C) 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ggp + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "path" + "path/filepath" + "runtime" + "strings" + "sync" + "time" + + "github.com/google/gapid/core/event/task" + "github.com/google/gapid/core/log" + bd "github.com/google/gapid/core/os/device" + "github.com/google/gapid/core/os/device/bind" + "github.com/google/gapid/core/os/device/remotessh" + "github.com/google/gapid/core/os/file" + "github.com/google/gapid/core/os/shell" +) + +// Binding represents an attached ggp ssh client +type Binding struct { + remotessh.Device + Gamelet string +} + +const ( + // Frequency at which to print scan errors + printScanErrorsEveryNSeconds = 120 +) + +var _ bind.Device = &Binding{} + +var ( + // Registry of all the discovered devices. + registry = bind.NewRegistry() + + // cache is a map of device names to fully resolved bindings. + cache = map[string]*Binding{} + cacheMutex sync.Mutex // Guards cache. +) + +// GGP is the path to the ggp executable. +var GGP file.Path + +// GGPExecutablePath returns the path to the ggp +// executable +func GGPExecutablePath() (file.Path, error) { + if !GGP.IsEmpty() { + return GGP, nil + } + ggpExe := "ggp" + search := []string{ggpExe} + + if ggpSDKPath := os.Getenv("GGP_SDK_PATH"); ggpSDKPath != "" { + search = append(search, + filepath.Join(ggpSDKPath, "dev", "bin", ggpExe)) + } + for _, path := range search { + if p, err := file.FindExecutable(path); err == nil { + GGP = p + return GGP, nil + } + } + + return file.Path{}, fmt.Errorf( + "ggp could not be found from GGP_SDK_PATH or PATH\n"+ + "GGP_SDK_PATH: %v\n"+ + "PATH: %v\n"+ + "search: %v", + os.Getenv("GGP_SDK_PATH"), os.Getenv("PATH"), search) +} + +func ggpDefaultRootConfigPath() (string, error) { + if runtime.GOOS == "windows" { + return os.Getenv("APPDATA"), nil + } + if p := os.Getenv("XDG_CONFIG_HOME"); p != "" { + return path.Clean(p), nil + } + if p := os.Getenv("HOME"); p != "" { + return path.Join(path.Clean(p), ".config"), nil + } + return "", fmt.Errorf("Can not find environment") +} + +type GGPConfiguration struct { + remotessh.Configuration + Gamelet string +} + +func getConfigs(ctx context.Context) ([]GGPConfiguration, error) { + configs := []GGPConfiguration{} + + ggpPath, err := GGPExecutablePath() + if err != nil { + return nil, log.Errf(ctx, err, "Could not find ggp executable to list gamelets") + } + cli := ggpPath.System() + + cmd := shell.Command(cli, "gamelet", "list") + gameletListOutBuf := &bytes.Buffer{} + gameletListErrBuf := &bytes.Buffer{} + + if err := cmd.Capture(gameletListOutBuf, gameletListErrBuf).Run(ctx); err != nil { + return nil, err + } + + t, err := ParseListOutput(gameletListOutBuf) + if err != nil { + return nil, log.Errf(ctx, err, "parse gamelet list") + } + for _, inf := range t.Rows { + if inf[3] != "RESERVED" && inf[3] != "IN_USE" { + continue + } + sshInitOutBuf := &bytes.Buffer{} + sshInitErrBuf := &bytes.Buffer{} + + if err := shell.Command(cli, "ssh", "init", "--gamelet", inf[1], "-s").Capture(sshInitOutBuf, sshInitErrBuf).Run(ctx); err != nil { + log.W(ctx, "'ggp ssh init --gamelet %v -s' finished with error: %v", inf[1], err) + continue + } + if sshInitErrBuf.Len() != 0 { + log.W(ctx, "'ggp ssh init --gamelet %v -s' finished with error: %v", inf[1], sshInitErrBuf.String()) + continue + } + envs := []string{ + "YETI_DISABLE_GUEST_ORC=1", + "YETI_DISABLE_STREAMER=1", + } + cfg := remotessh.Configuration{ + Name: inf[0], + Env: envs, + } + if err := json.Unmarshal(sshInitOutBuf.Bytes(), &cfg); err != nil { + log.W(ctx, "Failed at unmarshaling 'ggp ssh init --gamelet %v -s' output, fallback to use default config, err: %v", inf[1], err) + } + configs = append(configs, GGPConfiguration{cfg, inf[1]}) + } + return configs, nil +} + +// Monitor updates the registry with devices that are added and removed at the +// specified interval. Monitor returns once the context is cancelled. +func Monitor(ctx context.Context, r *bind.Registry, interval time.Duration) error { + unlisten := registry.Listen(bind.NewDeviceListener(r.AddDevice, r.RemoveDevice)) + defer unlisten() + + for _, d := range registry.Devices() { + r.AddDevice(ctx, d) + } + + var lastErrorPrinted time.Time + + for { + configs, err := getConfigs(ctx) + if err != nil { + return err + } + if err := scanDevices(ctx, configs); err != nil { + if time.Since(lastErrorPrinted).Seconds() > printScanErrorsEveryNSeconds { + log.E(ctx, "Couldn't scan devices: %v", err) + lastErrorPrinted = time.Now() + } + } else { + lastErrorPrinted = time.Time{} + } + + select { + case <-task.ShouldStop(ctx): + return nil + case <-time.After(interval): + } + } +} + +// Devices returns the list of attached GGP devices. +func Devices(ctx context.Context) ([]bind.Device, error) { + configs, err := getConfigs(ctx) + + if err != nil { + return nil, err + } + + if err := scanDevices(ctx, configs); err != nil { + return nil, err + } + devs := registry.Devices() + out := make([]bind.Device, len(devs)) + for i, d := range devs { + out[i] = d + } + return out, nil +} + +func deviceStillConnected(ctx context.Context, d *Binding) bool { + return d.Status(ctx) == bind.Status_Online +} + +func scanDevices(ctx context.Context, configurations []GGPConfiguration) error { + cacheMutex.Lock() + defer cacheMutex.Unlock() + allConfigs := make(map[string]bool) + + for _, cfg := range configurations { + allConfigs[cfg.Name] = true + + // If this device already exists, see if we + // can/have to remove it + if cached, ok := cache[cfg.Name]; ok { + if !deviceStillConnected(ctx, cached) { + delete(cache, cfg.Name) + registry.RemoveDevice(ctx, cached) + } + } else { + if device, err := remotessh.GetConnectedDevice(ctx, cfg.Configuration); err == nil { + dev := Binding{ + Device: device, + Gamelet: cfg.Gamelet, + } + device.Instance().Configuration.OS.Kind = bd.Stadia + registry.AddDevice(ctx, dev) + cache[cfg.Name] = &dev + } + } + } + + for name, dev := range cache { + if _, ok := allConfigs[name]; !ok { + delete(cache, name) + registry.RemoveDevice(ctx, *dev) + } + } + return nil +} + +// ListExecutables lists all executables. +// On GGP, executables may not have the executable bit set, +// so treat any file as executable +func (b Binding) ListExecutables(ctx context.Context, inPath string) ([]string, error) { + if inPath == "" { + inPath = b.GetURIRoot() + } + // 'find' may partially succeed. Redirect the error messages to /dev/null, only + // process the found files. + files, _ := b.Shell("find", `"`+inPath+`"`, "-mindepth", "1", "-maxdepth", "1", "-type", "f", "-printf", `%f\\n`, "2>/dev/null").Call(ctx) + scanner := bufio.NewScanner(strings.NewReader(files)) + out := []string{} + for scanner.Scan() { + _, file := path.Split(scanner.Text()) + out = append(out, file) + } + return out, nil +} + +// DefaultReplayCacheDir returns the default replay resource cache directory +// on a GGP device +func (b Binding) DefaultReplayCacheDir() string { + return "/mnt/developer/ggp/gapid/replay_cache" +} diff --git a/core/os/device/ggp/doc.go b/core/os/device/ggp/doc.go new file mode 100644 index 0000000000..573d3f4193 --- /dev/null +++ b/core/os/device/ggp/doc.go @@ -0,0 +1,17 @@ +// Copyright (C) 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package ggp contains code for binding to and controlling Stadia gamelets. +// https://stadia.dev +package ggp diff --git a/core/os/device/ggp/parse.go b/core/os/device/ggp/parse.go new file mode 100644 index 0000000000..fe3fa1780c --- /dev/null +++ b/core/os/device/ggp/parse.go @@ -0,0 +1,137 @@ +// Copyright (C) 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ggp + +import ( + "bufio" + "bytes" + "fmt" + "io" + "strings" +) + +// ParseList contains the output from ggp xxx list command +type ParsedList struct { + Header []string + Rows [][]string +} + +// ColumnByName returns the content of the table for the specified column in +// a list of string. +func (t ParsedList) ColumnByName(name string) ([]string, error) { + if len(t.Header) == 0 { + return []string{}, nil + } + ci := -1 + for i, h := range t.Header { + if h == name { + ci = i + } + } + if ci == -1 { + return nil, fmt.Errorf("Could not find column with name: %v", name) + } + ret := make([]string, len(t.Rows)) + for i, r := range t.Rows { + ret[i] = r[ci] + } + return ret, nil +} + +// ParseListOutput parses the output from ggp xxx list command. +// For example: +// blablabla +// header1 header2 header3 +// ======= ======= ======= +// value1 value2 value3 +func ParseListOutput(stdout *bytes.Buffer) (*ParsedList, error) { + reader := bufio.NewReader(stdout) + schemaLine := "" + contentLines := []string{} + fieldIndices := []int{} + for { + line, err := reader.ReadString('\n') + if err == io.EOF { + break + } + if err != nil { + break + } + line = strings.TrimSuffix(line, "\n") + if strings.HasPrefix(line, "no results") { + return &ParsedList{}, nil + } + if strings.HasPrefix(line, "==") { + line = strings.TrimSpace(line) + for i, _ := range line { + if i == 0 { + fieldIndices = append(fieldIndices, i) + continue + } + if line[i] == '=' && line[i-1] == ' ' { + fieldIndices = append(fieldIndices, i) + } + if line[i] != '=' && line[i] != ' ' { + return nil, fmt.Errorf("Separator line contains character other than '=' and blankspace") + } + } + continue + } + if len(fieldIndices) == 0 { + schemaLine = line + } else { + contentLines = append(contentLines, line) + } + } + + if len(schemaLine) == 0 { + return nil, fmt.Errorf("No table header (Column names) found") + } + + extractFieldsFromLine := func(line string) ([]string, error) { + ret := make([]string, 0, len(fieldIndices)) + for i, _ := range fieldIndices { + start := fieldIndices[i] + var end int + if i == len(fieldIndices)-1 { + end = len(line) + } else { + end = fieldIndices[i+1] + } + if start >= len(line) || end > len(line) { + return nil, fmt.Errorf("Unexpected length of line: %v, substr [%v:%d] failed", len(line), start, end) + } + ret = append(ret, strings.TrimSpace(line[start:end])) + } + return ret, nil + } + + schema, err := extractFieldsFromLine(schemaLine) + if err != nil { + return nil, err + } + rows := make([][]string, len(contentLines)) + for i, l := range contentLines { + fields, err := extractFieldsFromLine(l) + if err != nil { + return nil, err + } + rows[i] = fields + } + return &ParsedList{ + Header: schema, + Rows: rows, + }, nil +} diff --git a/core/os/device/remotessh/commands.go b/core/os/device/remotessh/commands.go index 3b8068e0a9..5fb589d778 100644 --- a/core/os/device/remotessh/commands.go +++ b/core/os/device/remotessh/commands.go @@ -167,7 +167,7 @@ func (b binding) createWindowsTempDirectory(ctx context.Context) (string, app.Cl // full path, and a function that can be called to clean up the directory. func (b binding) MakeTempDir(ctx context.Context) (string, app.Cleanup, error) { switch b.os { - case device.Linux, device.OSX: + case device.Linux, device.OSX, device.Stadia: return b.createPosixTempDirectory(ctx) case device.Windows: return b.createWindowsTempDirectory(ctx) @@ -196,10 +196,11 @@ func (b binding) PushFile(ctx context.Context, source, dest string) error { return err } mode := permission.Mode() - // If we are on windows pushing to Linux, we lose the executable + // If we are on windows pushing to Posix, we lose the executable // bit, get it back. if (b.os == device.Linux || - b.os == device.OSX) && + b.os == device.OSX || + b.os == device.Stadia) && runtime.GOOS == "windows" { mode |= 0550 } diff --git a/core/os/shell/command.go b/core/os/shell/command.go index 08d50a1891..173a9fd54f 100644 --- a/core/os/shell/command.go +++ b/core/os/shell/command.go @@ -137,7 +137,11 @@ func (cmd Cmd) Start(ctx context.Context) (Process, error) { } // Ready to start if cmd.Verbosity { - log.I(ctx, "Exec: %v", cmd) + extra := "" + if cmd.Dir != "" { + extra = fmt.Sprintf(" In %v", cmd.Dir) + } + log.I(ctx, "Exec: %v%s", cmd, extra) } return cmd.Target.Start(cmd) } diff --git a/core/vulkan/cc/include/vulkan/vulkan.h b/core/vulkan/cc/include/vulkan/vulkan.h index 2683666bcf..5abb7f27ff 100644 --- a/core/vulkan/cc/include/vulkan/vulkan.h +++ b/core/vulkan/cc/include/vulkan/vulkan.h @@ -235,6 +235,7 @@ typedef enum VkStructureType { VK_STRUCTURE_TYPE_DEDICATED_ALLOCATION_IMAGE_CREATE_INFO_NV = 1000026000, VK_STRUCTURE_TYPE_DEDICATED_ALLOCATION_BUFFER_CREATE_INFO_NV = 1000026001, VK_STRUCTURE_TYPE_DEDICATED_ALLOCATION_MEMORY_ALLOCATE_INFO_NV = 1000026002, + VK_STRUCTURE_TYPE_STREAM_DESCRIPTOR_SURFACE_CREATE_INFO_GGP = 1000049000, VK_STRUCTURE_TYPE_BEGIN_RANGE = VK_STRUCTURE_TYPE_APPLICATION_INFO, VK_STRUCTURE_TYPE_END_RANGE = VK_STRUCTURE_TYPE_LOADER_DEVICE_CREATE_INFO, VK_STRUCTURE_TYPE_RANGE_SIZE = (VK_STRUCTURE_TYPE_LOADER_DEVICE_CREATE_INFO - VK_STRUCTURE_TYPE_APPLICATION_INFO + 1), @@ -3689,6 +3690,30 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateAndroidSurfaceKHR( #endif #endif /* VK_USE_PLATFORM_ANDROID_KHR */ +#define VK_USE_PLATFORM_GGP 1 +#ifdef VK_USE_PLATFORM_GGP + +#define VK_GGP_stream_descriptor_surface 1 +#define VK_GGP_STREAM_DESCRIPTOR_SURFACE_SPEC_VERSION 1 +#define VK_GGP_STREAM_DESCRIPTOR_SURFACE_EXTENSION_NAME "VK_GGP_stream_descriptor_surface" +typedef VkFlags VkStreamDescriptorSurfaceCreateFlagsGGP; +typedef struct VkStreamDescriptorSurfaceCreateInfoGGP { + VkStructureType sType; + const void* pNext; + VkStreamDescriptorSurfaceCreateFlagsGGP flags; + uint32_t streamDescriptor; +} VkStreamDescriptorSurfaceCreateInfoGGP; +typedef VkResult (VKAPI_PTR *PFN_vkCreateStreamDescriptorSurfaceGGP)(VkInstance instance, const VkStreamDescriptorSurfaceCreateInfoGGP* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSurfaceKHR* pSurface); +#ifndef VK_NO_PROTOTYPES +VKAPI_ATTR VkResult VKAPI_CALL vkCreateStreamDescriptorSurfaceGGP( + VkInstance instance, + const VkStreamDescriptorSurfaceCreateInfoGGP* pCreateInfo, + const VkAllocationCallbacks* pAllocator, + VkSurfaceKHR* pSurface); +#endif + +#endif /* VK_USE_PLATFORM_GGP */ + #ifdef VK_USE_PLATFORM_WIN32_KHR #define VK_KHR_win32_surface 1 diff --git a/core/vulkan/vk_virtual_swapchain/cc/BUILD.bazel b/core/vulkan/vk_virtual_swapchain/cc/BUILD.bazel index b4c187999c..2911e3f2da 100644 --- a/core/vulkan/vk_virtual_swapchain/cc/BUILD.bazel +++ b/core/vulkan/vk_virtual_swapchain/cc/BUILD.bazel @@ -21,7 +21,10 @@ cc_library( "*.h", ]), copts = cc_copts() + select({ - "//tools/build:linux": ["-DVK_USE_PLATFORM_XCB_KHR"], + "//tools/build:linux": [ + "-DVK_USE_PLATFORM_XCB_KHR", + "-DVK_USE_PLATFORM_GGP", + ], "//tools/build:darwin": [], "//tools/build:windows": ["-DVK_USE_PLATFORM_WIN32_KHR"], # Android @@ -52,7 +55,10 @@ cc_library( "*.h", ]), copts = cc_copts() + select({ - "//tools/build:linux": ["-DVK_USE_PLATFORM_XCB_KHR"], + "//tools/build:linux": [ + "-DVK_USE_PLATFORM_XCB_KHR", + "-DVK_USE_PLATFORM_GGP", + ], "//tools/build:darwin": [], "//tools/build:windows": ["-DVK_USE_PLATFORM_WIN32_KHR"], # Android diff --git a/core/vulkan/vk_virtual_swapchain/cc/layer.cpp b/core/vulkan/vk_virtual_swapchain/cc/layer.cpp index 9813cec0ae..6b37eaa1f2 100644 --- a/core/vulkan/vk_virtual_swapchain/cc/layer.cpp +++ b/core/vulkan/vk_virtual_swapchain/cc/layer.cpp @@ -138,6 +138,9 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateInstance( #ifdef VK_USE_PLATFORM_ANDROID_KHR GET_PROC(vkCreateAndroidSurfaceKHR); #endif +#ifdef VK_USE_PLATFORM_GGP + GET_PROC(vkCreateStreamDescriptorSurfaceGGP); +#endif #ifdef VK_USE_PLATFORM_XCB_KHR GET_PROC(vkCreateXcbSurfaceKHR); #endif @@ -494,6 +497,8 @@ vkGetInstanceProcAddr(VkInstance instance, const char *funcName) { INTERCEPT_SURFACE(vkCreateWin32SurfaceKHR); INTERCEPT_SURFACE(vkCreateXcbSurfaceKHR); INTERCEPT_SURFACE(vkCreateXlibSurfaceKHR); + INTERCEPT_SURFACE(vkCreateStreamDescriptorSurfaceGGP); + #undef INTERCEPT_SURFACE // If we are calling a non-overloaded function then we have to // return the "next" in the chain. On vkCreateInstance we stored this in diff --git a/core/vulkan/vk_virtual_swapchain/cc/layer.h b/core/vulkan/vk_virtual_swapchain/cc/layer.h index d32a7a4646..051314a907 100644 --- a/core/vulkan/vk_virtual_swapchain/cc/layer.h +++ b/core/vulkan/vk_virtual_swapchain/cc/layer.h @@ -52,6 +52,9 @@ struct InstanceData { #ifdef VK_USE_PLATFORM_ANDROID_KHR PFN_vkCreateAndroidSurfaceKHR vkCreateAndroidSurfaceKHR; #endif +#ifdef VK_USE_PLATFORM_GGP + PFN_vkCreateStreamDescriptorSurfaceGGP vkCreateStreamDescriptorSurfaceGGP; +#endif #ifdef VK_USE_PLATFORM_XCB_KHR PFN_vkCreateXcbSurfaceKHR vkCreateXcbSurfaceKHR; #endif diff --git a/core/vulkan/vk_virtual_swapchain/cc/platform.cpp b/core/vulkan/vk_virtual_swapchain/cc/platform.cpp index cf00c08c34..48f9105a9a 100644 --- a/core/vulkan/vk_virtual_swapchain/cc/platform.cpp +++ b/core/vulkan/vk_virtual_swapchain/cc/platform.cpp @@ -59,6 +59,21 @@ void CreateSurface(const InstanceData* functions, VkInstance instance, } } #endif +#ifdef VK_USE_PLATFORM_GGP + { + auto pCreateInfo = + static_cast(data); + if (pCreateInfo->sType == + VK_STRUCTURE_TYPE_STREAM_DESCRIPTOR_SURFACE_CREATE_INFO_GGP) { + // Attempt to create ggp surface + if (functions->vkCreateStreamDescriptorSurfaceGGP( + instance, pCreateInfo, pAllocator, pSurface) != VK_SUCCESS) { + *pSurface = 0; + } + return; + } + } +#endif } } // namespace swapchain diff --git a/gapir/cc/surface.cpp b/gapir/cc/surface.cpp index 6e69cdf6c1..5b941fb7d2 100644 --- a/gapir/cc/surface.cpp +++ b/gapir/cc/surface.cpp @@ -173,6 +173,8 @@ void* createXcbWindow(uint32_t width, uint32_t height) { window_create_flag.Wait(); return window_info.window ? (void*)&window_info : nullptr; } + +static const int32_t stream_index = 0; #elif TARGET_OS == GAPID_OS_WINDOWS static Win32WindowInfo window_info; @@ -255,6 +257,8 @@ const void* CreateSurface(uint32_t width, uint32_t height, SurfaceType& type) { case SurfaceType::Unknown: type = SurfaceType::Xcb; return createXcbWindow(width, height); + case SurfaceType::Ggp: + return (const void*)&stream_index; #elif TARGET_OS == GAPID_OS_WINDOWS case SurfaceType::Win32: case SurfaceType::Unknown: diff --git a/gapir/cc/surface.h b/gapir/cc/surface.h index 701d127f75..8afd84c4fd 100644 --- a/gapir/cc/surface.h +++ b/gapir/cc/surface.h @@ -45,12 +45,7 @@ struct Win32WindowInfo { }; #endif -enum SurfaceType { - Unknown, - Android, - Win32, - Xcb, -}; +enum SurfaceType { Unknown, Android, Win32, Xcb, Ggp }; // Get the platform-specific data pointer to create the surface const void* CreateSurface(uint32_t width, uint32_t height, SurfaceType& type); diff --git a/gapis/api/templates/vulkan_gfx_api_extras.tmpl b/gapis/api/templates/vulkan_gfx_api_extras.tmpl index b836930ad2..0917910be7 100644 --- a/gapis/api/templates/vulkan_gfx_api_extras.tmpl +++ b/gapis/api/templates/vulkan_gfx_api_extras.tmpl @@ -345,74 +345,84 @@ bool Vulkan::replayCreateVkDeviceImpl(Stack* stack, size_val physicalDevice, VkAndroidSurfaceCreateInfoKHR androidInfo = {}; #elif TARGET_OS == GAPID_OS_LINUX VkXcbSurfaceCreateInfoKHR xcbInfo = {}; + VkStreamDescriptorSurfaceCreateInfoGGP ggpInfo = {}; #elif TARGET_OS == GAPID_OS_WINDOWS VkWin32SurfaceCreateInfoKHR win32Info = {}; #endif for (auto pNext = static_cast(newCreateInfo.pNext); pNext != nullptr; - pNext = static_cast(pNext->pNext)) { + pNext = static_cast(pNext->pNext)) { if (pNext->sType == swapchain::VIRTUAL_SWAPCHAIN_CREATE_PNEXT) { if (pNext->surfaceCreateInfo) { - // Copy any extra fields + // Copy any extra fields vsPNext = *pNext; gapir::SurfaceType target = gapir::SurfaceType::Unknown; - switch (*(uint32_t*)pNext->surfaceCreateInfo) { - case VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR: - target = gapir::SurfaceType::Android; - break; - // These are all linux, so just make an Xcb surface - case VK_STRUCTURE_TYPE_MIR_SURFACE_CREATE_INFO_KHR: - case VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR: - case VK_STRUCTURE_TYPE_XCB_SURFACE_CREATE_INFO_KHR: - case VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR: - target = gapir::SurfaceType::Xcb; - break; - case VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR: - target = gapir::SurfaceType::Win32; - } + switch (*(uint32_t*)pNext->surfaceCreateInfo) { + case VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR: + target = gapir::SurfaceType::Android; + break; - const void* data = CreateSurface(newCreateInfo.imageExtent.Width, - newCreateInfo.imageExtent.Height, target); - void* createInfo = nullptr; - if (data != nullptr) { - switch (target) { -#if TARGET_OS == GAPID_OS_ANDROID - case gapir::SurfaceType::Android: - createInfo = &androidInfo; - androidInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR; - androidInfo.window = (ANativeWindow*)data; + // These are all linux, so just make an Xcb surface + case VK_STRUCTURE_TYPE_MIR_SURFACE_CREATE_INFO_KHR: + case VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR: + case VK_STRUCTURE_TYPE_XCB_SURFACE_CREATE_INFO_KHR: + case VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR: + target = gapir::SurfaceType::Xcb; break; + case VK_STRUCTURE_TYPE_STREAM_DESCRIPTOR_SURFACE_CREATE_INFO_GGP: + target = gapir::SurfaceType::Ggp; + break; + case VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR: + target = gapir::SurfaceType::Win32; + } + + const void* data = CreateSurface(newCreateInfo.imageExtent.Width, + newCreateInfo.imageExtent.Height, target); + void* createInfo = nullptr; + if (data != nullptr) { + switch (target) { +#if TARGET_OS == GAPID_OS_ANDROID + case gapir::SurfaceType::Android: + createInfo = &androidInfo; + androidInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR; + androidInfo.window = (ANativeWindow*)data; + break; #elif TARGET_OS == GAPID_OS_LINUX - case gapir::SurfaceType::Xcb: - createInfo = &xcbInfo; - { - auto info = (XcbWindowInfo*)data; - xcbInfo.sType = VK_STRUCTURE_TYPE_XCB_SURFACE_CREATE_INFO_KHR; - xcbInfo.connection = info->connection; - xcbInfo.window = info->window; - } - break; + case gapir::SurfaceType::Xcb: + createInfo = &xcbInfo; + { + auto info = (XcbWindowInfo*)data; + xcbInfo.sType = VK_STRUCTURE_TYPE_XCB_SURFACE_CREATE_INFO_KHR; + xcbInfo.connection = info->connection; + xcbInfo.window = info->window; + } + break; + case gapir::SurfaceType::Ggp: + createInfo = &ggpInfo; + ggpInfo.sType = VK_STRUCTURE_TYPE_STREAM_DESCRIPTOR_SURFACE_CREATE_INFO_GGP; + ggpInfo.streamDescriptor = *(const uint32_t*)data; + break; #elif TARGET_OS == GAPID_OS_WINDOWS - case gapir::SurfaceType::Win32: - createInfo = &win32Info; - { - auto info = (Win32WindowInfo*)data; - win32Info.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR; - win32Info.hinstance = (HINSTANCE)info->instance; - win32Info.hwnd = (HWND)info->window; - } - break; + case gapir::SurfaceType::Win32: + createInfo = &win32Info; + { + auto info = (Win32WindowInfo*)data; + win32Info.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR; + win32Info.hinstance = (HINSTANCE)info->instance; + win32Info.hwnd = (HWND)info->window; + } + break; #endif - default: - break; - } - } - vsPNext.surfaceCreateInfo = createInfo; - vsPNext.pNext = newCreateInfo.pNext; - newCreateInfo.pNext = &vsPNext; - } - break; + default: + break; + } + } + vsPNext.surfaceCreateInfo = createInfo; + vsPNext.pNext = newCreateInfo.pNext; + newCreateInfo.pNext = &vsPNext; + } + break; } } diff --git a/gapis/api/vulkan/BUILD.bazel b/gapis/api/vulkan/BUILD.bazel index 87675d62e1..e9ed8463b4 100644 --- a/gapis/api/vulkan/BUILD.bazel +++ b/gapis/api/vulkan/BUILD.bazel @@ -25,6 +25,7 @@ filegroup( "linux/*.api", "windows/*.api", "android/*.api", + "ggp/*.api", ]), visibility = ["//visibility:public"], ) diff --git a/gapis/api/vulkan/api/enums.api b/gapis/api/vulkan/api/enums.api index fa473c58c9..bdb5f3e81b 100644 --- a/gapis/api/vulkan/api/enums.api +++ b/gapis/api/vulkan/api/enums.api @@ -330,6 +330,11 @@ enum VkStructureType { // Virtual Swapchain VK_STRUCTURE_TYPE_VIRTUAL_SWAPCHAIN_PNEXT = 0xFFFFFFAA, + + // @extension("VK_GGP_frame_token") + VK_STRUCTURE_TYPE_PRESENT_FRAME_TOKEN_GGP = 1000191000, + // @extension("VK_GGP_stream_descriptor_surface") + VK_STRUCTURE_TYPE_STREAM_DESCRIPTOR_SURFACE_CREATE_INFO_GGP = 1000049000, } @analyze_usage diff --git a/gapis/api/vulkan/extensions/khr_surface.api b/gapis/api/vulkan/extensions/khr_surface.api index 16db4fd56c..21df84079a 100644 --- a/gapis/api/vulkan/extensions/khr_surface.api +++ b/gapis/api/vulkan/extensions/khr_surface.api @@ -237,6 +237,8 @@ enum SurfaceType { SURFACE_TYPE_WAYLAND = 4 SURFACE_TYPE_XLIB = 5 SURFACE_TYPE_MIR = 6 + // RESERVED = 7 + SURFACE_TYPE_GGP = 8 } @internal class queueFamilySupports { diff --git a/gapis/api/vulkan/ggp/vulkan_ggp.api b/gapis/api/vulkan/ggp/vulkan_ggp.api new file mode 100644 index 0000000000..9df8976c2b --- /dev/null +++ b/gapis/api/vulkan/ggp/vulkan_ggp.api @@ -0,0 +1,56 @@ +// Copyright (C) 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ---------------------------------------------------------------------------- +// VK_GGP_stream_descriptor_surface +// ---------------------------------------------------------------------------- + +type VkFlags VkStreamDescriptorSurfaceCreateFlagsGGP +type u32 GgpStreamDescriptor + +@extension("VK_GGP_stream_descriptor_surface") +class VkStreamDescriptorSurfaceCreateInfoGGP { + VkStructureType sType + const void* pNext + VkStreamDescriptorSurfaceCreateFlagsGGP flags + GgpStreamDescriptor streamDescriptor +} + +@extension("VK_GGP_stream_descriptor_surface") +@indirect("VkInstance") +cmd VkResult vkCreateStreamDescriptorSurfaceGGP( + VkInstance instance, + const VkStreamDescriptorSurfaceCreateInfoGGP* pCreateInfo, + const VkAllocationCallbacks* pAllocator, + VkSurfaceKHR* pSurface) { + read(pCreateInfo[0:1]) + surface := new!SurfaceObject() + surface.Instance = instance + surface.Type = SURFACE_TYPE_GGP + // TODO: pAllocator + + handle := ? + pSurface[0] = handle + surface.VulkanHandle = handle + Surfaces[handle] = surface + + return ? +} + +@extension("VK_GGP_frame_token") +class VkPresentFrameTokenGGP { + VkStructureType sType + const void* pNext + u64 frameToken +} diff --git a/gapis/api/vulkan/replay.go b/gapis/api/vulkan/replay.go index 685ef8ae96..f1a7d4cec9 100644 --- a/gapis/api/vulkan/replay.go +++ b/gapis/api/vulkan/replay.go @@ -684,6 +684,10 @@ func (t *DisplayToSurface) Transform(ctx context.Context, id api.CmdID, cmd api. cmd.Extras().Observations().ApplyWrites(out.State().Memory.ApplicationPool()) surface := c.PSurface().MustRead(ctx, cmd, out.State(), nil) t.SurfaceTypes[uint64(surface)] = uint32(VkStructureType_VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR) + case *VkCreateStreamDescriptorSurfaceGGP: + cmd.Extras().Observations().ApplyWrites(out.State().Memory.ApplicationPool()) + surface := c.PSurface().MustRead(ctx, cmd, out.State(), nil) + t.SurfaceTypes[uint64(surface)] = uint32(VkStructureType_VK_STRUCTURE_TYPE_STREAM_DESCRIPTOR_SURFACE_CREATE_INFO_GGP) } out.MutateAndWrite(ctx, id, cmd) } diff --git a/gapis/api/vulkan/state_rebuilder.go b/gapis/api/vulkan/state_rebuilder.go index 265fcb55f5..8019bb5d35 100644 --- a/gapis/api/vulkan/state_rebuilder.go +++ b/gapis/api/vulkan/state_rebuilder.go @@ -607,6 +607,19 @@ func (sb *stateBuilder) createSurface(s SurfaceObjectʳ) { sb.MustAllocWriteData(s.VulkanHandle()).Ptr(), VkResult_VK_SUCCESS, )) + case SurfaceType_SURFACE_TYPE_GGP: + sb.write(sb.cb.VkCreateStreamDescriptorSurfaceGGP( + s.Instance(), + sb.MustAllocReadData(NewVkStreamDescriptorSurfaceCreateInfoGGP(sb.ta, + VkStructureType_VK_STRUCTURE_TYPE_STREAM_DESCRIPTOR_SURFACE_CREATE_INFO_GGP, + 0, // pNext + 0, // flags + 0, // streamDescriptor + )).Ptr(), + memory.Nullptr, + sb.MustAllocWriteData(s.VulkanHandle()).Ptr(), + VkResult_VK_SUCCESS, + )) } for phyDev, familyIndices := range s.PhysicalDeviceSupports().All() { for index, supported := range familyIndices.QueueFamilySupports().All() { diff --git a/gapis/api/vulkan/templates/vk_spy_helpers.cpp.tmpl b/gapis/api/vulkan/templates/vk_spy_helpers.cpp.tmpl index f4cbd99aed..ff012e2bf2 100644 --- a/gapis/api/vulkan/templates/vk_spy_helpers.cpp.tmpl +++ b/gapis/api/vulkan/templates/vk_spy_helpers.cpp.tmpl @@ -145,8 +145,6 @@ uint32_t VulkanSpy::SpyOverride_vkCreateInstance( get_instance_proc_addr(0, "vkCreateInstance")); mImports.pfn_vkCreateInstance = create_instance; - mImports.pfn_vkEnumerateInstanceExtensionProperties \ - = reinterpret_cast(get_instance_proc_addr(0, "vkEnumerateInstanceExtensionProperties")); if (create_instance == NULL) { return VkResult::VK_ERROR_INITIALIZATION_FAILED; @@ -159,6 +157,9 @@ uint32_t VulkanSpy::SpyOverride_vkCreateInstance( // Actually call vkCreateInstance, and keep track of the result. uint32_t result = create_instance(pCreateInfo, pAllocator, pInstance); + mImports.pfn_vkEnumerateInstanceExtensionProperties \ + = reinterpret_cast(get_instance_proc_addr(*pInstance, "vkEnumerateInstanceExtensionProperties")); + // Send a header with Vulkan info added if we haven't done so. const device::Drivers& drivers = this->SpyBase::device_instance()->configuration().drivers(); diff --git a/gapis/api/vulkan/vulkan.api b/gapis/api/vulkan/vulkan.api index 9d19710a96..6ba1cfc793 100644 --- a/gapis/api/vulkan/vulkan.api +++ b/gapis/api/vulkan/vulkan.api @@ -93,6 +93,7 @@ import "extensions/khr_draw_indirect_count.api" import "android/vulkan_android.api" import "linux/vulkan_linux.api" import "windows/vulkan_windows.api" +import "ggp/vulkan_ggp.api" import "synthetic.api" import "errors.api" @@ -207,6 +208,8 @@ sub ref!ExtensionSet supportedInstanceExtensions() { supported.ExtensionNames["VK_KHR_external_memory_capabilities"] = true supported.ExtensionNames["VK_KHR_external_semaphore_capabilities"] = true supported.ExtensionNames["VK_KHR_external_fence_capabilities"] = true + supported.ExtensionNames["VK_GGP_stream_descriptor_surface"] = true + supported.ExtensionNames["VK_GGP_frame_token"] = true return supported } @@ -229,6 +232,8 @@ sub ref!ExtensionSet supportedDeviceExtensions() { supported.ExtensionNames["VK_KHR_maintenance1"] = true supported.ExtensionNames["VK_KHR_maintenance2"] = true supported.ExtensionNames["VK_KHR_maintenance3"] = true + supported.ExtensionNames["VK_GGP_stream_descriptor_surface"] = true + supported.ExtensionNames["VK_GGP_frame_token"] = true supported.ExtensionNames["VK_AMD_draw_indirect_count"] = true supported.ExtensionNames["VK_KHR_draw_indirect_count"] = true return supported diff --git a/gapis/trace/desktop/BUILD.bazel b/gapis/trace/desktop/BUILD.bazel index 765b06ca5f..bf51130388 100644 --- a/gapis/trace/desktop/BUILD.bazel +++ b/gapis/trace/desktop/BUILD.bazel @@ -16,14 +16,23 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "go_default_library", - srcs = ["trace.go"], + srcs = [ + "ggp_trace.go", + "trace.go", + ], importpath = "github.com/google/gapid/gapis/trace/desktop", visibility = ["//visibility:public"], deps = [ "//core/app:go_default_library", + "//core/app/crash:go_default_library", + "//core/event/task:go_default_library", + "//core/log:go_default_library", "//core/os/device:go_default_library", "//core/os/device/bind:go_default_library", + "//core/os/device/ggp:go_default_library", "//core/os/process:go_default_library", + "//core/os/shell:go_default_library", + "//core/text:go_default_library", "//core/vulkan/loader:go_default_library", "//gapii/client:go_default_library", "//gapis/service:go_default_library", diff --git a/gapis/trace/desktop/ggp_trace.go b/gapis/trace/desktop/ggp_trace.go new file mode 100644 index 0000000000..1308c5e57e --- /dev/null +++ b/gapis/trace/desktop/ggp_trace.go @@ -0,0 +1,600 @@ +// Copyright (C) 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package desktop + +import ( + "bytes" + "context" + "os" + "path" + "regexp" + "runtime" + "strconv" + "strings" + "sync" + + "fmt" + + "github.com/google/gapid/core/app" + "github.com/google/gapid/core/app/crash" + "github.com/google/gapid/core/event/task" + "github.com/google/gapid/core/log" + "github.com/google/gapid/core/os/device/bind" + "github.com/google/gapid/core/os/device/ggp" + "github.com/google/gapid/core/os/process" + "github.com/google/gapid/core/os/shell" + "github.com/google/gapid/core/text" + "github.com/google/gapid/core/vulkan/loader" + gapii "github.com/google/gapid/gapii/client" + "github.com/google/gapid/gapis/service" + "github.com/google/gapid/gapis/trace/tracer" +) + +type GGPTracer struct { + DesktopTracer + applications []string + ggpBinding ggp.Binding + packageNameMutex sync.RWMutex + // package names is a mapping from package URIs to package names. + packageNames map[string]string +} + +func NewGGPTracer(ctx context.Context, dev bind.Device) (*GGPTracer, error) { + cols, err := getListOutputColumns(ctx, "application", nil, "Display Name") + if err != nil { + return nil, log.Errf(ctx, err, "getting application list") + } + apps := cols[0] + if yd, ok := dev.(ggp.Binding); ok { + return &GGPTracer{DesktopTracer{dev.(bind.Device)}, apps, yd, sync.RWMutex{}, map[string]string{}}, nil + } else { + return nil, fmt.Errorf("Trying to use a GGP device as a non-ggp device") + } + +} + +// TraceConfiguration returns the device's supported trace configuration. +func (t *GGPTracer) TraceConfiguration(ctx context.Context) (*service.DeviceTraceConfiguration, error) { + apis := make([]*service.TraceTypeCapabilities, 0, 1) + if len(t.b.Instance().GetConfiguration().GetDrivers().GetVulkan().GetPhysicalDevices()) > 0 { + apis = append(apis, tracer.VulkanTraceOptions()) + } + + return &service.DeviceTraceConfiguration{ + Apis: apis, + ServerLocalPath: false, + CanSpecifyCwd: false, + CanUploadApplication: false, + CanSpecifyEnv: true, + PreferredRootUri: "/mnt/developer/", + HasCache: false, + }, nil +} + +// StartOnDevice runs the application on the given remote device, +// with the given path and options, waits for the "Bound on port {port}" string +// to be printed to stdout, and then returns the port number. +func (t *GGPTracer) StartOnDevice(ctx context.Context, name string, opts *process.StartOptions) (int, error) { + // Append extra environment variable values + errChan := make(chan error, 1) + portChan := make(chan string, 1) + + stdout := process.NewPortWatcher(portChan, opts) + + c, cancel := task.WithCancel(ctx) + defer cancel() + crash.Go(func() { + stdout.WaitForFile(c) + }) + + splitUri := strings.Split(name, ":") + if len(splitUri) != 2 { + return 0, fmt.Errorf("Invalid trace URI %+v", name) + } + + fmt.Fprintf(os.Stderr, "Trying to start %+v\n", splitUri) + ggpExecutable, err := ggp.GGPExecutablePath() + + if err != nil { + return 0, err + } + + crash.Go(func() { + cmdArgs := []string{ + "run", + "--gamelet", + t.ggpBinding.Gamelet, + "--application", + splitUri[1], + } + execArgStr := strings.Join(opts.Args, " ") + + if strings.HasPrefix(splitUri[0], "package=") { + _, pkg, _, err := parsePackageURI(ctx, splitUri[0]) + if err != nil { + log.E(ctx, "Start tracing on invalid uri: %v", err) + } + // If the tracing target is specified by package ID, we don't know + // the executable from the URI, only append the execution arguments + // to the --cmd flag. + if len(execArgStr) != 0 { + cmdArgs = append(cmdArgs, "--cmd", execArgStr) + } + cmdArgs = append(cmdArgs, "--package", pkg) + } else { + // If the tracing target is specified by file path on the gamelet, + // prepend the executable, which is part of the URI, to the argument + // list for --cmd flag. + execArgStr = splitUri[0] + " " + execArgStr + cmdArgs = append(cmdArgs, "--cmd", execArgStr) + } + + envs := "" + for _, e := range opts.Env.Vars() { + if envs != "" { + envs += ";" + } + envs += text.Quote([]string{e})[0] + } + + if envs != "" { + cmdArgs = append(cmdArgs, "--env", envs) + } + + execCmd := ggpExecutable.System() + + // On Windows, if we are tracing, we want the "opening browser" + // window to show up nicely for the user. + // Also ggp_cli does not always play nice with being a subprocess. + // To combat this: run the application in a separate console + // window. The user gets their output, and we dont hang the CLI. + if runtime.GOOS == "windows" { + // The first quoted argument is the title of the window. + // Force the first argument to be "GGP Command" + cmdArgs = append([]string{ + `/C`, `start`, `GGP Command`, `/Wait`, execCmd, + }, cmdArgs...) + execCmd = "cmd.exe" + } + + cmd := shell.Command(execCmd, cmdArgs...). + Capture(stdout, opts.Stderr). + Verbose() + if opts.Verbose { + cmd = cmd.Verbose() + } + errChan <- cmd.Run(ctx) + }) + + for { + select { + case port := <-portChan: + p, err := strconv.Atoi(port) + if err != nil { + return 0, err + } + return opts.Device.SetupLocalPort(ctx, p) + case err := <-errChan: + if err != nil { + return 0, err + } + } + } +} + +func (t *GGPTracer) SetupTrace(ctx context.Context, o *service.TraceOptions) (tracer.Process, app.Cleanup, error) { + env := shell.NewEnv() + cleanup, portFile, err := loader.SetupTrace(ctx, t.b, t.b.Instance().Configuration.ABIs[0], env) + if err != nil { + cleanup.Invoke(ctx) + return nil, nil, err + } + r := regexp.MustCompile("'.+'|\".+\"|\\S+") + args := r.FindAllString(o.AdditionalCommandLineArgs, -1) + + for _, x := range o.Environment { + env.Add(x) + } + + boundPort, err := t.StartOnDevice(ctx, o.GetUri(), &process.StartOptions{ + Env: env, + Args: args, + PortFile: portFile, + WorkingDir: o.Cwd, + Device: t.b, + }) + + if err != nil { + cleanup.Invoke(ctx) + return nil, nil, err + } + process := &gapii.Process{Port: boundPort, Device: t.b, Options: tracer.GapiiOptions(o)} + return process, cleanup, nil +} + +// FindTraceTargets implements the tracer.Tracer interface. +// GGP tracer supports two forms of URI to specify tracing targets: +// 1) File path and Application form: :, e.g.: +// "/mnt/developer/cube:MyApplication" +// 2) Package ID (w/o Project Name) and Application form: +// If the package is in current project: "package=:" +// If the package is in another project: "package=/:" +// e.g.: +// "package=ba843a36f96451b237138769fc141733pks1:MyApplication" +// "package=/PACKAGE/ba843a36f96451b237138769fc141733pks1:MyApplication" +// Valid charactors for and are: [a-zA-Z0-9\s\_\-], +// valid charactors for are: [a-z0-9]. +// In case the given |str| is not a valid URI, error will be returned. +func (t *GGPTracer) FindTraceTargets(ctx context.Context, str string) ([]*tracer.TraceTargetTreeNode, error) { + fileData := strings.Split(str, ":") + + if len(fileData) != 2 { + return nil, fmt.Errorf("The trace target is not valid") + } + + if strings.HasPrefix(fileData[0], "package=") { + proj, pkg, app, err := parsePackageURI(ctx, str) + if err != nil { + return nil, err + } + if len(pkg) == 0 { + return nil, fmt.Errorf("Package not specified") + } + if len(app) == 0 { + return nil, fmt.Errorf("Application not specified") + } + tttn := &tracer.TraceTargetTreeNode{ + Name: pkg + ":" + app, + URI: buildPackageURI(proj, pkg, app), + TraceURI: buildPackageURI(proj, pkg, app), + Children: nil, + Parent: buildPackageURI(proj, pkg, ""), + ExecutableName: pkg, + } + return []*tracer.TraceTargetTreeNode{tttn}, nil + } + + isFile, err := t.b.IsFile(ctx, fileData[0]) + if err != nil { + return nil, err + } + if !isFile { + return nil, fmt.Errorf("Trace target is not an executable file %+v", fileData[0]) + } + dir, file := path.Split(fileData[0]) + + if dir == "" { + dir = "." + str = "./" + file + } + finalUri := "" + for _, x := range t.applications { + if x == fileData[1] { + finalUri = fileData[0] + ":" + fileData[1] + break + } + } + + if finalUri == "" { + return nil, fmt.Errorf("Invalid application %+v", fileData[1]) + } + + tttn := &tracer.TraceTargetTreeNode{ + Name: fileData[1], + Icon: nil, + URI: finalUri, + TraceURI: finalUri, + Children: nil, + Parent: file, + ApplicationName: "", + ExecutableName: file, + } + + return []*tracer.TraceTargetTreeNode{tttn}, nil +} + +// GetTraceTargetNode implements the tracer.Tracer interface. +func (t *GGPTracer) GetTraceTargetNode(ctx context.Context, uri string, iconDensity float32) (*tracer.TraceTargetTreeNode, error) { + if uri == "" { + return &tracer.TraceTargetTreeNode{ + Name: "", + Icon: nil, + URI: uri, + TraceURI: "", + Children: []string{"/", "package="}, + Parent: "", + ApplicationName: "", + ExecutableName: "", + }, nil + } + if strings.HasPrefix(uri, "/") { + return t.getFileTargetNode(ctx, uri, iconDensity) + } + if strings.HasPrefix(uri, "package=") { + return t.getPackageTargetNode(ctx, uri, iconDensity) + } + return nil, log.Errf(ctx, nil, "Unrecoginized uri: %v", uri) +} + +func (t *GGPTracer) getPackageTargetNode(ctx context.Context, uri string, iconDensity float32) (*tracer.TraceTargetTreeNode, error) { + children := []string{} + proj, pkg, app, err := parsePackageURI(ctx, uri) + if err != nil { + return nil, log.Errf(ctx, err, "getting trace target node for package") + } + if len(proj) == 0 && len(pkg) == 0 { + cols, err := getListOutputColumns(ctx, "package", nil, "ID", "Display Name") + if err == nil { + pkgIds := cols[0] + pkgNms := cols[1] + + for i, p := range pkgIds { + children = append(children, buildPackageURI("", p, "")) + t.setPackageName(p, pkgNms[i]) + } + children = append(children, buildPackageURI("/", "", "")) + return &tracer.TraceTargetTreeNode{ + Name: "Packages", + URI: buildPackageURI("", "", ""), + TraceURI: "", + Children: children, + Parent: "", + }, nil + } else { + log.E(ctx, "Error at listing packages in the current project: %v", err) + } + } + + if proj == "/" { + cols, err := getListOutputColumns(ctx, "project", nil, "Display Name") + if err == nil { + projs := cols[0] + for _, p := range projs { + match, _ := regexp.MatchString(`^[a-zA-Z0-9\_\-\s]+$`, p) + if match { + children = append(children, buildPackageURI(p, "", "")) + } + } + return &tracer.TraceTargetTreeNode{ + Name: "Other projects", + URI: buildPackageURI("/", "", ""), + TraceURI: "", + Children: children, + Parent: buildPackageURI("", "", ""), + }, nil + } else { + log.E(ctx, "Error at listing projects: %v", err) + } + } + + if len(pkg) == 0 { + cols, err := getListOutputColumns(ctx, "package", []string{"--project=" + proj}, "ID", "Display Name") + if err == nil { + pkgIds := cols[0] + pkgNms := cols[1] + for i, p := range pkgIds { + children = append(children, buildPackageURI(proj, p, "")) + t.setPackageName(p, pkgNms[i]) + } + return &tracer.TraceTargetTreeNode{ + Name: proj, + URI: buildPackageURI(proj, "", ""), + TraceURI: "", + Children: children, + Parent: buildPackageURI("/", "", ""), + }, nil + } else { + log.E(ctx, "Error at listing packages in project: %v: %v", proj, err) + } + } + + nm := pkg + if pn, ok := t.getPackageName(pkg); ok { + if len(pn) > 0 { + nm = pn + } + } + if len(app) == 0 { + for _, a := range t.applications { + children = append(children, buildPackageURI(proj, pkg, a)) + } + return &tracer.TraceTargetTreeNode{ + Name: nm, + URI: buildPackageURI(proj, pkg, ""), + TraceURI: buildPackageURI("", "", ""), + Children: children, + Parent: buildPackageURI(proj, "", ""), + }, nil + } + + return &tracer.TraceTargetTreeNode{ + Name: nm + ":" + app, + URI: buildPackageURI(proj, pkg, app), + TraceURI: buildPackageURI(proj, pkg, app), + Children: nil, + Parent: buildPackageURI(proj, pkg, ""), + ExecutableName: pkg, + }, nil +} + +func (t *GGPTracer) getFileTargetNode(ctx context.Context, uri string, iconDensity float32) (*tracer.TraceTargetTreeNode, error) { + dirs := []string{} + files := []string{} + var err error + + traceUri := "" + if uri == "" { + uri = t.b.GetURIRoot() + } + fileData := strings.Split(uri, ":") + + p := fileData[0] + app := "" + if len(fileData) > 1 { + app = fileData[1] + } + + isFile, err := t.b.IsFile(ctx, p) + if err != nil { + return nil, err + } + children := []string{} + if !isFile { + dirs, err = t.b.ListDirectories(ctx, uri) + if err != nil { + return nil, err + } + + files, err = t.b.ListExecutables(ctx, uri) + if err != nil { + return nil, err + } + + children = append(dirs, files...) + + for i := range children { + children[i] = path.Join(uri, children[i]) + // path.Join will clean off preceding . + if uri == "." { + children[i] = "./" + children[i] + } + } + } else { + traceUri = p + if app != "" { + traceUri = traceUri + ":" + app + } else { + for _, a := range t.applications { + children = append(children, p+":"+a) + } + } + } + + dir, file := path.Split(uri) + name := file + if name == "" { + name = dir + } + + tttn := &tracer.TraceTargetTreeNode{ + Name: name, + Icon: nil, + URI: uri, + TraceURI: traceUri, + Children: children, + Parent: dir, + ApplicationName: "", + ExecutableName: file, + } + + return tttn, nil +} + +func (t *GGPTracer) setPackageName(uri, name string) { + t.packageNameMutex.Lock() + defer t.packageNameMutex.Unlock() + t.packageNames[uri] = name +} + +func (t *GGPTracer) getPackageName(uri string) (string, bool) { + t.packageNameMutex.RLock() + defer t.packageNameMutex.RUnlock() + if _, ok := t.packageNames[uri]; !ok { + return "", false + } + return t.packageNames[uri], true +} + +// getListOutputColumns calls ggp list commands for the given listName, along +// with extra arguments specified in extras. The contents of the given column +// names will be returned. The content of each column will be represented in +// a list of strings, in the same order shown in the ggp list output. And the +// content of columns will be returned in the order of the column names. +func getListOutputColumns(ctx context.Context, listName string, extras []string, columnNames ...string) ([][]string, error) { + ggpPath, err := ggp.GGPExecutablePath() + if err != nil { + return nil, log.Errf(ctx, err, "getting %v", listName) + } + executable := ggpPath.System() + args := []string{listName, "list"} + if len(extras) != 0 { + args = append(args, extras...) + } + cmd := shell.Command(executable, args...) + outBuf := &bytes.Buffer{} + errBuf := &bytes.Buffer{} + if err := cmd.Capture(outBuf, errBuf).Run(ctx); err != nil { + return nil, log.Errf(ctx, err, "run %v list getting command", listName) + } + t, err := ggp.ParseListOutput(outBuf) + if err != nil { + return nil, log.Errf(ctx, err, "parse %v list", listName) + } + result := make([][]string, len(columnNames)) + for i, c := range columnNames { + l, err := t.ColumnByName(c) + if err != nil { + return nil, log.Errf(ctx, err, "getting %v %v(s)", listName, c) + } + result[i] = l + } + return result, nil +} + +// parsePackageURI parses the project display name, package ID and application +// string from the given URI in forms "package=:" or +// "package=/:". The URI is allowed to +// be partially complete, which means the URI may not be a complete URI +// targeting to a valid tracing target, but must starts with "package=". If the +// a field in the URI is missing, empty string will be returned for the +// corresponding return value. One special case only used internally is: +// "package=/", which returns proj="/", pkg="", app="". +func parsePackageURI(ctx context.Context, uri string) (proj, pkg, app string, err error) { + re := regexp.MustCompile(`package=(\/$|[a-zA-Z0-9\_\-\s]+\/)?([a-z0-9\_\-\s]+)?(\:[a-zA-Z0-9\s\_\-]+$)?`) + groups := re.FindStringSubmatch(uri) + if len(groups) != 4 { + err = log.Errf(ctx, nil, "cannot parse uri: %v as package uri", uri) + return + } + if groups[1] == "/" { + proj = "/" + } else { + proj = strings.Trim(groups[1], "/") + } + pkg = groups[2] + app = strings.TrimLeft(groups[3], ":") + return +} + +// buildPackageURI takes project, package ID, and application to build an URI. +// The result is guaranteed can be successfully parsed by parsePackageURI +// defined above. One special case only used internally is: proj="/", pkg="", +// app="", which will return "package=/". +func buildPackageURI(proj, pkg, app string) string { + uri := "package=" + if len(proj) != 0 { + uri = uri + proj + if proj != "/" { + uri = uri + "/" + } + } + if len(pkg) != 0 { + uri = uri + pkg + if len(app) != 0 { + uri = uri + ":" + app + } + } + return uri +} diff --git a/gapis/trace/manager.go b/gapis/trace/manager.go index 25d1a99120..6998fc8940 100644 --- a/gapis/trace/manager.go +++ b/gapis/trace/manager.go @@ -55,6 +55,13 @@ func (m *Manager) createTracer(ctx context.Context, dev bind.Device) { defer m.mutex.Unlock() if dev.Instance().GetConfiguration().GetOS().GetKind() == device.Android { m.tracers[deviceID] = android.NewTracer(dev) + } else if dev.Instance().GetConfiguration().GetOS().GetKind() == device.Stadia { + if tracer, err := desktop.NewGGPTracer(ctx, dev); err == nil { + m.tracers[deviceID] = tracer + } else { + log.E(ctx, "Could not resolve GGP device %+v, trying as desktop", err) + m.tracers[deviceID] = desktop.NewTracer(dev) + } } else { m.tracers[deviceID] = desktop.NewTracer(dev) }