com.google.gapid.views.GeometryView.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gapid.views.GeometryView.java

Source

/*
 * Copyright (C) 2017 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 com.google.gapid.views;

import static com.google.gapid.glviewer.Geometry.isPolygon;
import static com.google.gapid.util.Loadable.MessageType.Error;
import static com.google.gapid.util.Loadable.MessageType.Info;
import static com.google.gapid.util.Paths.meshAfter;
import static com.google.gapid.widgets.Widgets.createSeparator;
import static com.google.gapid.widgets.Widgets.createToggleToolItem;
import static com.google.gapid.widgets.Widgets.createToolItem;
import static com.google.gapid.widgets.Widgets.exclusiveSelection;

import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.Uninterruptibles;
import com.google.gapid.Server.GapisInitException;
import com.google.gapid.glviewer.Geometry;
import com.google.gapid.glviewer.Geometry.DisplayMode;
import com.google.gapid.glviewer.Viewer;
import com.google.gapid.glviewer.camera.CylindricalCameraModel;
import com.google.gapid.glviewer.camera.IsoSurfaceCameraModel;
import com.google.gapid.glviewer.geo.Model;
import com.google.gapid.models.AtomStream;
import com.google.gapid.models.Capture;
import com.google.gapid.models.Models;
import com.google.gapid.models.Strings;
import com.google.gapid.proto.service.Service.CommandRange;
import com.google.gapid.proto.service.gfxapi.GfxAPI;
import com.google.gapid.proto.service.path.Path;
import com.google.gapid.proto.service.vertex.Vertex;
import com.google.gapid.proto.stringtable.Stringtable;
import com.google.gapid.rpclib.futures.FutureController;
import com.google.gapid.rpclib.futures.SingleInFlight;
import com.google.gapid.rpclib.rpccore.Rpc;
import com.google.gapid.rpclib.rpccore.RpcException;
import com.google.gapid.server.Client;
import com.google.gapid.server.Client.DataUnavailableException;
import com.google.gapid.util.Messages;
import com.google.gapid.util.Streams;
import com.google.gapid.util.UiErrorCallback;
import com.google.gapid.widgets.GlComposite;
import com.google.gapid.widgets.LoadablePanel;
import com.google.gapid.widgets.Theme;
import com.google.gapid.widgets.Widgets;
import com.google.protobuf.ByteString;

import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.logging.Logger;

/**
 * View that displays the 3D geometry of the last draw call within the current selection.
 */
public class GeometryView extends Composite implements Tab, Capture.Listener, AtomStream.Listener {
    private static final Logger LOG = Logger.getLogger(GeometryView.class.getName());
    private static final Vertex.Semantic POSITION_0 = Vertex.Semantic.newBuilder()
            .setType(Vertex.Semantic.Type.Position).setIndex(0).build();
    private static final Vertex.Semantic NORMAL_0 = Vertex.Semantic.newBuilder()
            .setType(Vertex.Semantic.Type.Normal).setIndex(0).build();
    protected static final Vertex.BufferFormat POS_NORM_XYZ_F32 = Vertex.BufferFormat.newBuilder()
            .addStreams(Vertex.StreamFormat.newBuilder().setSemantic(POSITION_0).setFormat(Streams.FMT_XYZ_F32))
            .addStreams(Vertex.StreamFormat.newBuilder().setSemantic(NORMAL_0).setFormat(Streams.FMT_XYZ_F32))
            .build();
    private static final Stringtable.Msg NO_MESH_ERR = Strings.create("ERR_MESH_NOT_AVAILABLE");

    private final Client client;
    private final Models models;
    private final FutureController rpcController = new SingleInFlight();
    protected final LoadablePanel<GlComposite> loading;
    private final Geometry geometry = new Geometry();
    private final IsoSurfaceCameraModel camera = new IsoSurfaceCameraModel(new CylindricalCameraModel());
    private final Viewer viewer = new Viewer(camera);
    private final GlComposite canvas;
    private ToolItem originalModelItem, facetedModelItem;
    private Model originalModel, facetedModel;
    private Geometry.DisplayMode displayMode = Geometry.DisplayMode.TRIANGLES;
    private Geometry.DisplayMode desiredDisplayMode = Geometry.DisplayMode.TRIANGLES;
    private ToolItem renderAsTriangles, renderAsLines, renderAsPoints;

    public GeometryView(Composite parent, Client client, Models models, Widgets widgets) {
        super(parent, SWT.NONE);
        this.client = client;
        this.models = models;

        setLayout(new GridLayout(2, false));

        ToolBar toolbar = createToolbar(widgets.theme);
        loading = LoadablePanel.create(this, widgets, panel -> createCanvas(panel));
        canvas = loading.getContents();

        toolbar.setLayoutData(new GridData(SWT.LEFT, SWT.FILL, false, true));
        loading.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

        models.capture.addListener(this);
        models.atoms.addListener(this);
        addListener(SWT.Dispose, e -> {
            models.capture.removeListener(this);
            models.atoms.removeListener(this);
        });

        viewer.addMouseListeners(canvas);

        originalModelItem.setEnabled(false);
        facetedModelItem.setEnabled(false);
    }

    private GlComposite createCanvas(Composite parent) {
        GlComposite result = new GlComposite(parent);
        result.addListener(viewer);
        return result;
    }

    private ToolBar createToolbar(Theme theme) {
        ToolBar bar = new ToolBar(this, SWT.VERTICAL | SWT.FLAT);
        createToolItem(bar, theme.yUp(), e -> {
            boolean zUp = geometry.toggleZUp();
            ((ToolItem) e.widget).setImage(zUp ? theme.zUp() : theme.yUp());
            updateViewer();
        }, "Toggle Y/Z up");
        createToolItem(bar, theme.windingCCW(), e -> {
            Viewer.Winding winding = viewer.toggleWinding();
            ((ToolItem) e.widget)
                    .setImage((winding == Viewer.Winding.CCW) ? theme.windingCCW() : theme.windingCW());
            updateViewer();
        }, "Toggle triangle winding");
        createSeparator(bar);
        exclusiveSelection(renderAsTriangles = createToggleToolItem(bar, theme.wireframeNone(), e -> {
            desiredDisplayMode = displayMode = Geometry.DisplayMode.TRIANGLES;
            updateRenderable();
        }, "Render as triangles"), renderAsLines = createToggleToolItem(bar, theme.wireframeAll(), e -> {
            desiredDisplayMode = displayMode = Geometry.DisplayMode.LINES;
            updateRenderable();
        }, "Render as lines"), renderAsPoints = createToggleToolItem(bar, theme.pointCloud(), e -> {
            desiredDisplayMode = displayMode = Geometry.DisplayMode.POINTS;
            updateRenderable();
        }, "Render as points"));
        createSeparator(bar);
        exclusiveSelection(originalModelItem = createToggleToolItem(bar, theme.smooth(), e -> {
            setModel(originalModel);
        }, "Use original normals"), facetedModelItem = createToggleToolItem(bar, theme.faceted(), e -> {
            setModel(facetedModel);
        }, "Use computed per-face normals"));
        createSeparator(bar);
        createToolItem(bar, theme.cullingDisabled(), e -> {
            Viewer.Culling culling = viewer.toggleCulling();
            ((ToolItem) e.widget)
                    .setImage((culling == Viewer.Culling.ON) ? theme.cullingEnabled() : theme.cullingDisabled());
            updateViewer();
        }, "Toggle backface culling");
        createSeparator(bar);
        exclusiveSelection(createToggleToolItem(bar, theme.lit(), e -> {
            viewer.setShading(Viewer.Shading.LIT);
            updateViewer();
        }, "Render with a lit shader"), createToggleToolItem(bar, theme.flat(), e -> {
            viewer.setShading(Viewer.Shading.FLAT);
            updateViewer();
        }, "Render with a flat shader (silhouette)"), createToggleToolItem(bar, theme.normals(), e -> {
            viewer.setShading(Viewer.Shading.NORMALS);
            updateViewer();
        }, "Render normals"));
        return bar;
    }

    @Override
    public Control getControl() {
        return this;
    }

    @Override
    public void reinitialize() {
        updateModels(false);
    }

    @Override
    public void onCaptureLoadingStart(boolean maintainState) {
        updateModels(true);
    }

    @Override
    public void onCaptureLoaded(GapisInitException error) {
        if (error != null) {
            loading.showMessage(Error, Messages.CAPTURE_LOAD_FAILURE);
        }
    }

    @Override
    public void onAtomsLoaded() {
        updateModels(false);
    }

    @Override
    public void onAtomsSelected(CommandRange range) {
        updateModels(false);
    }

    private void updateModels(boolean assumeLoading) {
        if (!assumeLoading && models.atoms.isLoaded()) {
            Path.Command drawPath = models.atoms.getLastSelectedDrawCall();
            if (drawPath == null) {
                loading.showMessage(Info, Messages.SELECT_DRAW_CALL);
            } else {
                fetchMeshes(drawPath);
            }
        } else {
            loading.showMessage(Info, Messages.LOADING_CAPTURE);
        }
    }

    private void fetchMeshes(Path.Command path) {
        loading.startLoading();
        ListenableFuture<Model> originalFuture = fetchModel(
                meshAfter(path, Path.MeshOptions.getDefaultInstance(), POS_NORM_XYZ_F32));
        ListenableFuture<Model> facetedFuture = fetchModel(
                meshAfter(path, Path.MeshOptions.newBuilder().setFaceted(true).build(), POS_NORM_XYZ_F32));
        Rpc.listen(Futures.successfulAsList(originalFuture, facetedFuture), rpcController,
                new UiErrorCallback<List<Model>, List<Model>, String>(this, LOG) {
                    @Override
                    protected ResultOrError<List<Model>, String> onRpcThread(Rpc.Result<List<Model>> result)
                            throws RpcException, ExecutionException {
                        List<Model> modelList = result.get();
                        if (modelList.get(0) == null && modelList.get(1) == null) {
                            // Both failed, so get the error from the original model's call.
                            try {
                                Uninterruptibles.getUninterruptibly(originalFuture);
                            } catch (ExecutionException e) {
                                if (e.getCause() instanceof DataUnavailableException) {
                                    return error(e.getCause().getMessage());
                                } else {
                                    throw e;
                                }
                            }
                            // Should not get here, the future cannot both fail and succeed.
                            throw new AssertionError("Future both failed and succeeded");
                        } else if (modelList.get(0) != null && modelList.get(0).getNormals() == null) {
                            // The original model has no normals, but the geometry doesn't support faceted normals.
                            return success(Lists.newArrayList(null, modelList.get(0)));
                        } else if (modelList.get(0) != null && !isPolygon(modelList.get(0).getPrimitive())) {
                            // TODO: if gapis returns an error for the faceted request, this is not needed.
                            return success(Lists.newArrayList(modelList.get(0), null));
                        } else {
                            return success(modelList);
                        }
                    }

                    @Override
                    protected void onUiThreadSuccess(List<Model> modelList) {
                        update(modelList);
                    }

                    @Override
                    protected void onUiThreadError(String error) {
                        loading.showMessage(Error, error);
                    }
                });
    }

    private ListenableFuture<Model> fetchModel(Path.Any path) {
        return Futures.transformAsync(client.get(path), value -> fetchModel(value.getMesh()));
    }

    private static ListenableFuture<Model> fetchModel(GfxAPI.Mesh mesh) {
        Vertex.Buffer vb = mesh.getVertexBuffer();
        float[] positions = null;
        float[] normals = null;

        for (Vertex.Stream stream : vb.getStreamsList()) {
            switch (stream.getSemantic().getType()) {
            case Position:
                positions = byteStringToFloatArray(stream.getData());
                break;
            case Normal:
                normals = byteStringToFloatArray(stream.getData());
                break;
            default:
                // Ignore.
            }
        }

        GfxAPI.DrawPrimitive primitive = mesh.getDrawPrimitive();
        if (positions == null || (normals == null && isPolygon(primitive))) {
            return Futures.immediateFailedFuture(new DataUnavailableException(NO_MESH_ERR));
        }

        int[] indices = mesh.getIndexBuffer().getIndicesList().stream().mapToInt(x -> x).toArray();
        Model model = new Model(primitive, positions, normals, indices);
        return Futures.immediateFuture(model);
    }

    private static float[] byteStringToFloatArray(ByteString bytes) {
        byte[] data = bytes.toByteArray();
        FloatBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer();
        float[] out = new float[data.length / 4];
        buffer.get(out);
        return out;
    }

    protected void update(List<Model> modelList) {
        originalModel = modelList.get(0);
        facetedModel = modelList.get(1);
        loading.stopLoading();
        originalModelItem.setEnabled(originalModel != null);
        facetedModelItem.setEnabled(facetedModel != null);
        if (originalModel != null) {
            setModel(originalModel);
            originalModelItem.setSelection(true);
            facetedModelItem.setSelection(false);
        } else {
            setModel(facetedModel);
            originalModelItem.setSelection(false);
            facetedModelItem.setSelection(true);
        }
    }

    protected void setModel(Model model) {
        DisplayMode newDisplayMode = desiredDisplayMode;
        switch (model.getPrimitive()) {
        case TriangleStrip:
        case Triangles:
            renderAsTriangles.setEnabled(true);
            renderAsLines.setEnabled(true);
            break;
        case LineLoop:
        case LineStrip:
        case Lines:
            renderAsTriangles.setEnabled(false);
            renderAsLines.setEnabled(true);
            if (newDisplayMode == DisplayMode.TRIANGLES) {
                newDisplayMode = DisplayMode.LINES;
            }
            break;
        case Points:
            renderAsTriangles.setEnabled(false);
            renderAsLines.setEnabled(false);
            newDisplayMode = DisplayMode.POINTS;
            break;
        default:
            // Ignore.
        }

        setDisplayMode(newDisplayMode);
        geometry.setModel(model);
        updateRenderable();
    }

    private void setDisplayMode(DisplayMode newMode) {
        renderAsTriangles.setSelection(newMode == DisplayMode.TRIANGLES);
        renderAsLines.setSelection(newMode == DisplayMode.LINES);
        renderAsPoints.setSelection(newMode == DisplayMode.POINTS);
        displayMode = newMode;
    }

    private void updateRenderable() {
        // Repaint will happen below.
        viewer.setRenderable(geometry.asRenderable(displayMode));
        updateViewer();
    }

    private void updateViewer() {
        camera.setEmitter(geometry.getEmitter());
        canvas.paint();
    }
}