| // 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 |
| } |