| // Copyright (C) 2015 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 client |
| |
| import ( |
| "fmt" |
| "image" |
| "image/color" |
| "os" |
| "path/filepath" |
| "time" |
| |
| "android.googlesource.com/platform/tools/gpu/atexit" |
| "android.googlesource.com/platform/tools/gpu/log" |
| "android.googlesource.com/platform/tools/gpu/service" |
| "github.com/google/gxui" |
| "github.com/google/gxui/drivers/gl" |
| "github.com/google/gxui/math" |
| "github.com/google/gxui/themes/dark" |
| ) |
| |
| type Config struct { |
| DataPath string |
| Gapis string |
| GXUIDebug bool |
| InitialCapture string |
| ReplayDevice string |
| } |
| |
| type app struct { |
| Config |
| } |
| |
| func createPanels(appCtx *ApplicationContext, window gxui.Window) gxui.Control { |
| theme, driver := appCtx.Theme(), appCtx.Theme().Driver() |
| |
| vSplitter := theme.CreateSplitterLayout() |
| vSplitter.SetOrientation(gxui.Vertical) |
| |
| { |
| holder := theme.CreatePanelHolder() |
| holder.AddPanel(CreateFramesPanel(appCtx), "Frames") |
| holder.AddPanel(CreateProfilerPanel(appCtx), "Profile") |
| vSplitter.AddChild(holder) |
| } |
| |
| { |
| hSplitter := theme.CreateSplitterLayout() |
| hSplitter.SetOrientation(gxui.Horizontal) |
| { |
| holder := theme.CreatePanelHolder() |
| holder.AddPanel(CreateCommandsPanel(appCtx), "Commands") |
| hSplitter.AddChild(holder) |
| } |
| { |
| holder := theme.CreatePanelHolder() |
| holder.AddPanel(CreateColorBufferPanel(appCtx), "Color") |
| holder.AddPanel(CreateDepthBufferPanel(appCtx), "Depth") |
| holder.AddPanel(theme.CreateLinearLayout(), "Stencil") |
| hSplitter.AddChild(holder) |
| hSplitter.SetChildWeight(holder, 2.0) |
| } |
| vSplitter.AddChild(hSplitter) |
| vSplitter.SetChildWeight(hSplitter, 2.0) |
| } |
| |
| { |
| hSplitter := theme.CreateSplitterLayout() |
| hSplitter.SetOrientation(gxui.Horizontal) |
| { |
| holder := theme.CreatePanelHolder() |
| holder.AddPanel(CreateStatePanel(appCtx), "State") |
| if appCtx.GXUIDebug { |
| holder.AddPanel(CreateGxuiDebug(appCtx, window, driver), "GXUI debug") |
| } |
| hSplitter.AddChild(holder) |
| } |
| { |
| holder := theme.CreatePanelHolder() |
| holder.AddPanel(CreateMemoryPanel(appCtx), "Memory") |
| holder.AddPanel(CreateImageViewerPanel(appCtx), "Image") |
| holder.AddPanel(CreateDocsPanel(appCtx), "Docs") |
| holder.AddPanel(CreateLogPanel(appCtx), "Log") |
| hSplitter.AddChild(holder) |
| } |
| vSplitter.AddChild(hSplitter) |
| } |
| |
| return vSplitter |
| } |
| |
| type capture struct { |
| id service.CaptureId |
| info service.Capture |
| } |
| |
| func (d *capture) String() string { return d.info.Name } |
| |
| func createCaptureList(appCtx *ApplicationContext) gxui.DropDownList { |
| theme, r := appCtx.Theme(), appCtx.Rpc() |
| |
| adapter := gxui.CreateDefaultAdapter() |
| |
| list := theme.CreateDropDownList() |
| list.SetBubbleOverlay(appCtx.DropDownOverlay()) |
| list.SetAdapter(adapter) |
| list.OnSelectionChanged(func(item gxui.AdapterItem) { |
| appCtx.LoadCapture(item.(*capture).id, true) |
| }) |
| |
| go func() { |
| logger := appCtx.Logger().Fork().Enter("CreateCaptureList") |
| ids, err := r.GetCaptures(logger) |
| if err != nil { |
| return |
| } |
| captures := make([]*capture, 0, len(ids)) |
| for _, id := range ids { |
| id := id |
| go func() { |
| if info, err := r.ResolveCapture(logger, id); err == nil { |
| appCtx.Run(func() { |
| captures = append(captures, &capture{id, info}) |
| adapter.SetItems(captures) |
| if list.Selected() == nil && info.Name == appCtx.InitialCapture { |
| list.Select(captures[len(captures)-1]) |
| } |
| }) |
| } |
| }() |
| } |
| }() |
| return list |
| } |
| |
| type device struct { |
| id service.DeviceId |
| info service.Device |
| } |
| |
| func (d *device) String() string { return d.info.Name } |
| |
| func createDeviceList(appCtx *ApplicationContext) gxui.DropDownList { |
| theme, r := appCtx.Theme(), appCtx.Rpc() |
| driver := theme.Driver() |
| |
| adapter := gxui.CreateDefaultAdapter() |
| wanted := appCtx.ReplayDevice |
| |
| list := theme.CreateDropDownList() |
| list.SetBubbleOverlay(appCtx.DropDownOverlay()) |
| list.SetAdapter(adapter) |
| list.OnSelectionChanged(func(item gxui.AdapterItem) { |
| appCtx.SelectDevice(item.(*device).id) |
| wanted = "" |
| }) |
| |
| list.OnAttach(func() { |
| go func() { |
| logger := appCtx.Logger().Fork().Enter("DeviceListUpdate") |
| |
| for list.Attached() { // While the list control is visible |
| if ids, err := r.GetDevices(logger); err == nil { |
| devices := make([]*device, 0, len(ids)) |
| found := -1 |
| for _, id := range ids { |
| if info, err := r.ResolveDevice(logger, id); err == nil { |
| devices = append(devices, &device{id, info}) |
| if info.Name == wanted { |
| found = len(devices) - 1 |
| wanted = "" |
| } |
| } |
| } |
| |
| appCtx.Run(func() { |
| adapter.SetItems(devices) |
| if found >= 0 { |
| list.Select(devices[found]) |
| } |
| }) |
| } |
| |
| var selected service.DeviceId |
| driver.CallSync(func() { selected = appCtx.SelectedDevice() }) |
| |
| if !selected.Valid() { |
| time.Sleep(250 * time.Millisecond) |
| } else { |
| time.Sleep(30 * time.Second) |
| } |
| } |
| }() |
| }) |
| |
| return list |
| } |
| |
| func createToolbar(appCtx *ApplicationContext) gxui.Control { |
| theme := appCtx.Theme() |
| |
| takeCapture := theme.CreateButton() |
| takeCapture.SetText("Take capture") |
| takeCapture.OnClick(func(gxui.MouseEvent) { |
| CreateTakeCaptureDialog(appCtx) |
| }) |
| |
| captureLabel := theme.CreateLabel() |
| captureLabel.SetText("Capture: ") |
| captureList := createCaptureList(appCtx) |
| |
| deviceLabel := theme.CreateLabel() |
| deviceLabel.SetText("Device: ") |
| deviceList := createDeviceList(appCtx) |
| |
| layout := theme.CreateLinearLayout() |
| layout.SetDirection(gxui.LeftToRight) |
| layout.AddChild(takeCapture) |
| layout.AddChild(captureLabel) |
| layout.AddChild(captureList) |
| layout.AddChild(deviceLabel) |
| layout.AddChild(deviceList) |
| return layout |
| } |
| |
| func loadTiming(appCtx *ApplicationContext) { |
| deviceID := appCtx.SelectedDevice() |
| captureID := appCtx.CaptureID() |
| |
| go func() { |
| logger := appCtx.Logger().Fork().Enter("Replay: timing") |
| appCtx.timingInfo = service.TimingInfo{} |
| |
| timingInfoID, err := appCtx.rpc.GetTimingInfo(logger, deviceID, captureID, |
| service.TimingMaskTimingPerFrame|service.TimingMaskTimingPerDrawCall|service.TimingMaskTimingPerCommand) |
| if err != nil { |
| return |
| } |
| |
| timingInfo, err := appCtx.rpc.ResolveTimingInfo(logger, timingInfoID) |
| if err != nil { |
| return |
| } |
| |
| timingPerCommand := make(map[uint64]uint64) |
| for _, t := range timingInfo.PerCommand { |
| timingPerCommand[t.AtomId] = t.Nanoseconds |
| } |
| |
| appCtx.timingInfo = timingInfo |
| appCtx.timingPerCommand = timingPerCommand |
| appCtx.onTimingInfoUpdated.Fire() |
| }() |
| } |
| |
| func DoReplay(appCtx *ApplicationContext) { |
| driver, r := appCtx.Theme().Driver(), appCtx.Rpc() |
| deviceID := appCtx.SelectedDevice() |
| captureID := appCtx.CaptureID() |
| atomID := appCtx.SelectedAtomID() |
| settings := service.RenderSettings{ |
| MaxWidth: 65536, |
| MaxHeight: 65536, |
| Wireframe: appCtx.Wireframe(), |
| } |
| |
| if atomID == InvalidAtomID { |
| return |
| } |
| |
| atom := appCtx.Atoms()[atomID] |
| apiID := atom.Info.Api |
| |
| go func() { |
| logger := appCtx.Logger().Fork().Enter("Replay: color-buffer") |
| imageID, err := r.GetFramebufferColor(logger, deviceID, captureID, apiID, uint64(atomID), settings) |
| if err != nil { |
| return |
| } |
| |
| imageInfo, err := r.ResolveImageInfo(logger, imageID) |
| if err != nil { |
| return |
| } |
| |
| imageData, err := r.ResolveBinary(logger, imageInfo.Data) |
| if err != nil { |
| return |
| } |
| |
| width, height := int(imageInfo.Width), int(imageInfo.Height) |
| |
| var img *image.RGBA |
| if width > 0 && height > 0 { |
| img = image.NewRGBA(image.Rect(0, 0, width, height)) |
| img.Pix = []byte(imageData.Data) |
| } |
| |
| appCtx.Run(func() { |
| if appCtx.colorBuffer != nil { |
| appCtx.colorBuffer.Release() |
| } |
| appCtx.colorBuffer = nil |
| if img != nil { |
| appCtx.colorBuffer = driver.CreateTexture(img, 1) |
| appCtx.colorBuffer.SetFlipY(true) |
| } |
| appCtx.onColorBufferUpdate.Fire() |
| }) |
| |
| }() |
| |
| go func() { |
| logger := appCtx.Logger().Fork().Enter("Replay: depth-buffer") |
| imageID, err := r.GetFramebufferDepth(logger, deviceID, captureID, apiID, uint64(atomID)) |
| if err != nil { |
| return |
| } |
| |
| imageInfo, err := r.ResolveImageInfo(logger, imageID) |
| if err != nil { |
| return |
| } |
| |
| imageData, err := r.ResolveBinary(logger, imageInfo.Data) |
| if err != nil { |
| return |
| } |
| |
| buffer := imageData.Data |
| |
| width, height := int(imageInfo.Width), int(imageInfo.Height) |
| |
| var img *image.RGBA |
| if width > 0 && height > 0 { |
| img = image.NewRGBA(image.Rect(0, 0, width, height)) |
| for y := 0; y < height; y++ { |
| for x := 0; x < width; x++ { |
| r, g, b, a := float32(buffer[0]), float32(buffer[1])/255.0, float32(buffer[2])/65025.0, float32(buffer[3])/160581375.0 |
| depth := (r + g + b + a) / 255.0 |
| buffer = buffer[4:] |
| d := 0.01 / (1.0 - depth) |
| c := color.RGBA{ |
| R: byte(math.Cosf(d+math.TwoPi*0.000)*127.0 + 128.0), |
| G: byte(math.Cosf(d+math.TwoPi*0.333)*127.0 + 128.0), |
| B: byte(math.Cosf(d+math.TwoPi*0.666)*127.0 + 128.0), |
| A: byte(0xFF), |
| } |
| img.Set(x, y, c) |
| } |
| } |
| } |
| |
| appCtx.Run(func() { |
| if appCtx.depthBuffer != nil { |
| appCtx.depthBuffer.Release() |
| } |
| appCtx.depthBuffer = nil |
| if img != nil { |
| appCtx.depthBuffer = driver.CreateTexture(img, 1) |
| appCtx.depthBuffer.SetFlipY(true) |
| } |
| appCtx.onDepthBufferUpdate.Fire() |
| }) |
| }() |
| } |
| |
| type EnableDebugger interface { |
| EnableDebug(bool) |
| } |
| |
| func (a app) main(driver gxui.Driver) { |
| if a.GXUIDebug { |
| if d, ok := driver.(EnableDebugger); ok { |
| d.EnableDebug(true) |
| } |
| } |
| |
| theme := dark.CreateTheme(driver) |
| appCtx, err := CreateApplicationContext(theme, a.Config) |
| if err != nil { |
| fmt.Printf("Could not create application context: %v\n", err) |
| os.Exit(1) |
| } |
| |
| // Create the log file |
| logPath, _ := filepath.Abs(filepath.Join(a.DataPath, "..", "logs", "client.log")) |
| fmt.Printf("Client log file created at: %s\n", logPath) |
| logFile, err := log.File(logPath) |
| if err != nil { |
| panic(err) |
| } |
| atexit.Register(logFile.Close, time.Second) |
| appCtx.Logger().Add(logFile) |
| |
| window := theme.CreateWindow(800, 600, "Main") |
| |
| layout := theme.CreateLinearLayout() |
| layout.SetDirection(gxui.TopToBottom) |
| layout.AddChild(createToolbar(appCtx)) |
| layout.AddChild(createPanels(appCtx, window)) |
| |
| window.OnClose(driver.Terminate) |
| window.AddChild(layout) |
| window.AddChild(appCtx.DropDownOverlay()) |
| window.AddChild(appCtx.ToolTipOverlay()) |
| |
| // Perhaps add button to disable this? |
| appCtx.OnAtomSelected(appCtx.RequestReplay) |
| appCtx.OnAtomsUpdated(appCtx.LoadHierarchy) |
| appCtx.OnAtomsUpdated(func() { loadTiming(appCtx) }) |
| appCtx.OnRequestReplay(func() { DoReplay(appCtx) }) |
| appCtx.OnAtomSelected(appCtx.LoadState) |
| } |
| |
| func Run(config Config) { |
| app := app{Config: config} |
| gl.StartDriver(app.main) |
| } |