blob: 392262126c94069678bf026ed6b1f01be3b7d95e [file] [log] [blame]
// Copyright (C) 2016 The Android Open Source Project
//
// 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 main
import (
"flag"
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"io"
"os"
"path/filepath"
"android.googlesource.com/platform/tools/gpu/client/video"
"android.googlesource.com/platform/tools/gpu/framework/app"
"android.googlesource.com/platform/tools/gpu/framework/file"
"android.googlesource.com/platform/tools/gpu/framework/log"
"android.googlesource.com/platform/tools/gpu/framework/math/sint"
"android.googlesource.com/platform/tools/gpu/framework/task"
"android.googlesource.com/platform/tools/gpu/gapid/atom"
"android.googlesource.com/platform/tools/gpu/gapid/service"
"android.googlesource.com/platform/tools/gpu/gapid/service/path"
"android.googlesource.com/platform/tools/gpu/tools/robotester/font"
img "android.googlesource.com/platform/tools/gpu/gapid/image"
)
type videoFlags struct {
flag.FlagSet
replayFlags
fps int
out string
maxWidth int
maxHeight int
startFrame int
endFrame int
outputImages bool
}
func init() {
verb := &app.Verb{
Name: "video",
ShortHelp: "Produce a video of a .gfxtrace file",
}
videoFlags := videoFlags{}
videoFlags.replayFlags.bind(verb)
verb.Flags.IntVar(&videoFlags.maxWidth, "max-width", 1024, "maximum video width")
verb.Flags.IntVar(&videoFlags.maxHeight, "max-height", 800, "maximum video height")
verb.Flags.IntVar(&videoFlags.fps, "fps", 5, "frames per second")
verb.Flags.IntVar(&videoFlags.startFrame, "start-frame", 0, "frame to start capture from")
verb.Flags.IntVar(&videoFlags.endFrame, "end-frame", -1, "frame to end capture on: -1 for last frame")
verb.Flags.StringVar(&videoFlags.out, "o", "", "output video path.")
verb.Flags.BoolVar(&videoFlags.outputImages, "output-images", false, "If set then output each image to its own file in the given directory")
verb.Run = func(ctx log.Context, flags flag.FlagSet) error {
videoFlags.FlagSet = flags
return doVideo(ctx, videoFlags)
}
app.AddVerb(verb)
}
func doVideo(ctx log.Context, flags videoFlags) error {
if flags.NArg() != 1 {
app.Usage(ctx, "Exactly one gfx trace file expected, got %d", flags.NArg())
return nil
}
filepath, err := filepath.Abs(flags.Arg(0))
if err != nil {
return ctx.V("File", flags.Arg(0)).WrapError(err, "Finding file")
}
client, err := getGapis(ctx, flags.gapis, flags.gapir)
if err != nil {
return ctx.WrapError(err, "Failed to connect to the GAPIS server")
}
defer client.Close(ctx)
capture, err := client.LoadCapture(ctx, filepath)
if err != nil {
return ctx.V("capture", filepath).WrapError(err, "LoadCapture")
}
device, err := getDevice(ctx, client, flags.replayFlags)
if err != nil {
return err
}
boxedAtoms, err := client.Get(ctx, capture.Atoms())
if err != nil {
return ctx.WrapError(err, "Acquiring the capture's atoms")
}
atoms := boxedAtoms.(*atom.List).Atoms
// Count the number of frames
frameCount := 0
for _, a := range atoms {
if a.AtomFlags().IsEndOfFrame() {
frameCount++
}
}
ctx.Printf("Frames: %d", frameCount)
// Get all the rendered frames
const workers = 32
events := &task.Events{}
pool, shutdown := task.Pool(0, workers)
defer shutdown(ctx)
executor := task.Batch(pool, events)
rendered := make([]*image.NRGBA, frameCount)
errors := make([]error, frameCount)
atomIndices := make([]int, frameCount)
frameIndex := 0
framesToSkip := flags.startFrame
modifyFrame := func(frame *image.NRGBA, frameNumber int, width int, height int) {
if frame.Bounds().Dx() != width || frame.Bounds().Dy() != height {
src, rect := frame, image.Rect(0, 0, width, height)
frame = image.NewNRGBA(rect)
draw.Draw(frame, rect, src, image.ZP, draw.Src)
}
str := fmt.Sprintf("Frame: %d, atom: %d", frameNumber, atomIndices[frameNumber])
font.DrawString(str, frame, image.Pt(4, 4), color.Black)
font.DrawString(str, frame, image.Pt(2, 2), color.White)
}
for i, a := range atoms {
if a.AtomFlags().IsEndOfFrame() {
atom, index := capture.Atoms().Index(uint64(i)), frameIndex
atomIndices[frameIndex] = i
frameIndex++
if framesToSkip > 0 {
framesToSkip--
continue
}
if flags.endFrame >= 0 && index > flags.endFrame {
break
}
executor(ctx, func(ctx log.Context) error {
if frame, err := getFrame(ctx, flags, atom, device, client); err == nil {
if flags.outputImages {
modifyFrame(frame, index, frame.Bounds().Dx(), frame.Bounds().Dy())
outFile := flags.out
if outFile == "" {
outFile = file.Abs(filepath).ChangeExt("").System()
}
out, err := os.Create(fmt.Sprintf("%sFrame%d.png", outFile, index))
if err != nil {
errors[index] = err
return nil
}
err = png.Encode(out, frame)
if err != nil {
errors[index] = err
return nil
}
} else {
rendered[index] = frame
}
} else {
errors[index] = err
}
return nil
})
}
}
events.Wait(ctx)
if flags.endFrame >= 0 {
frameCount = flags.endFrame + 1
}
frameCount = frameCount - flags.startFrame
if flags.outputImages {
for i := flags.startFrame; i < frameCount; i++ {
if err := errors[i]; err != nil {
ctx.I("atom", i).Fail(err, "")
}
}
return nil
}
// Get the max width and height
width, height := 0, 0
for i := flags.startFrame; i < frameCount; i++ {
if frame := rendered[i]; frame != nil {
width = sint.Max(width, frame.Bounds().Dx())
height = sint.Max(height, frame.Bounds().Dy())
}
}
// Video dimensions must be divisible by two.
if (width & 1) != 0 {
width++
}
if (height & 1) != 0 {
height++
}
ctx.Printf("Max dimensions: (%d, %d)", width, height)
// Start an encoder
frames, video, err := video.Encode(ctx, video.Settings{FPS: flags.fps})
if err != nil {
return err
}
for i := flags.startFrame; i < frameCount; i++ {
if err := errors[i]; err != nil {
ctx.I("atom", i).Fail(err, "")
continue
}
frame := rendered[i]
modifyFrame(frame, i, width, height)
frames <- frame
}
close(frames)
out := flags.out
if out == "" {
out = file.Abs(filepath).ChangeExt(".mp4").System()
}
mpg, err := os.Create(out)
if err != nil {
return err
}
defer mpg.Close()
_, err = io.Copy(mpg, video)
return err
}
func getFrame(ctx log.Context, flags videoFlags, atom *path.Atom, device *path.Device, client service.Service) (*image.NRGBA, error) {
ctx = ctx.I("atom", int(atom.Index))
settings := service.RenderSettings{MaxWidth: uint32(flags.maxWidth), MaxHeight: uint32(flags.maxHeight)}
iip, err := client.GetFramebufferColor(ctx, device, atom, settings)
if err != nil {
return nil, err
}
iio, err := client.Get(ctx, iip)
if err != nil {
return nil, err
}
ii := iio.(*img.Info)
dataO, err := client.Get(ctx, ii.Data)
if err != nil {
return nil, err
}
w, h, data := int(ii.Width), int(ii.Height), dataO.([]byte)
ctx = ctx.I("width", w).I("height", h).V("format", ii.Format)
if ii.Width == 0 || ii.Height == 0 {
return nil, ctx.AsError("Framebuffer has zero dimensions")
}
data, err = img.Convert(data, w, h, ii.Format, img.RGBA())
if err != nil {
return nil, ctx.WrapError(err, "Failed to convert frame to RGBA")
}
stride := w * 4
return &image.NRGBA{
Rect: image.Rect(0, 0, w, h),
Stride: stride,
Pix: flip(data, stride),
}, nil
}
func flip(data []byte, stride int) []byte {
out := make([]byte, len(data))
for i, c := 0, len(data)/stride; i < c; i++ {
copy(out[(c-i-1)*stride:(c-i)*stride], data[i*stride:])
}
return out
}