1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17
18package android.filterpacks.ui;
19
20import android.filterfw.core.Filter;
21import android.filterfw.core.FilterContext;
22import android.filterfw.core.FilterSurfaceView;
23import android.filterfw.core.Frame;
24import android.filterfw.core.FrameFormat;
25import android.filterfw.core.GenerateFieldPort;
26import android.filterfw.core.GenerateFinalPort;
27import android.filterfw.core.GLEnvironment;
28import android.filterfw.core.GLFrame;
29import android.filterfw.core.MutableFrameFormat;
30import android.filterfw.core.ShaderProgram;
31import android.filterfw.format.ImageFormat;
32
33import android.view.SurfaceHolder;
34
35import android.util.Log;
36
37/**
38 * @hide
39 */
40public class SurfaceRenderFilter extends Filter implements SurfaceHolder.Callback {
41
42    private final int RENDERMODE_STRETCH   = 0;
43    private final int RENDERMODE_FIT       = 1;
44    private final int RENDERMODE_FILL_CROP = 2;
45
46    /** Required. Sets the destination filter surface view for this
47     * node.
48     */
49    @GenerateFinalPort(name = "surfaceView")
50    private FilterSurfaceView mSurfaceView;
51
52    /** Optional. Control how the incoming frames are rendered onto the
53     * output. Default is FIT.
54     * RENDERMODE_STRETCH: Just fill the output surfaceView.
55     * RENDERMODE_FIT: Keep aspect ratio and fit without cropping. May
56     * have black bars.
57     * RENDERMODE_FILL_CROP: Keep aspect ratio and fit without black
58     * bars. May crop.
59     */
60    @GenerateFieldPort(name = "renderMode", hasDefault = true)
61    private String mRenderModeString;
62
63    private boolean mIsBound = false;
64
65    private ShaderProgram mProgram;
66    private GLFrame mScreen;
67    private int mRenderMode = RENDERMODE_FIT;
68    private float mAspectRatio = 1.f;
69
70    private int mScreenWidth;
71    private int mScreenHeight;
72
73    private boolean mLogVerbose;
74    private static final String TAG = "SurfaceRenderFilter";
75
76    public SurfaceRenderFilter(String name) {
77        super(name);
78
79        mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE);
80    }
81
82    @Override
83    public void setupPorts() {
84        // Make sure we have a SurfaceView
85        if (mSurfaceView == null) {
86            throw new RuntimeException("NULL SurfaceView passed to SurfaceRenderFilter");
87        }
88
89        // Add input port
90        addMaskedInputPort("frame", ImageFormat.create(ImageFormat.COLORSPACE_RGBA));
91    }
92
93    public void updateRenderMode() {
94        if (mRenderModeString != null) {
95            if (mRenderModeString.equals("stretch")) {
96                mRenderMode = RENDERMODE_STRETCH;
97            } else if (mRenderModeString.equals("fit")) {
98                mRenderMode = RENDERMODE_FIT;
99            } else if (mRenderModeString.equals("fill_crop")) {
100                mRenderMode = RENDERMODE_FILL_CROP;
101            } else {
102                throw new RuntimeException("Unknown render mode '" + mRenderModeString + "'!");
103            }
104        }
105        updateTargetRect();
106    }
107
108    @Override
109    public void prepare(FilterContext context) {
110        // Create identity shader to render, and make sure to render upside-down, as textures
111        // are stored internally bottom-to-top.
112        mProgram = ShaderProgram.createIdentity(context);
113        mProgram.setSourceRect(0, 1, 1, -1);
114        mProgram.setClearsOutput(true);
115        mProgram.setClearColor(0.0f, 0.0f, 0.0f);
116
117        updateRenderMode();
118
119        // Create a frame representing the screen
120        MutableFrameFormat screenFormat = ImageFormat.create(mSurfaceView.getWidth(),
121                                                             mSurfaceView.getHeight(),
122                                                             ImageFormat.COLORSPACE_RGBA,
123                                                             FrameFormat.TARGET_GPU);
124        mScreen = (GLFrame)context.getFrameManager().newBoundFrame(screenFormat,
125                                                                   GLFrame.EXISTING_FBO_BINDING,
126                                                                   0);
127    }
128
129    @Override
130    public void open(FilterContext context) {
131        // Bind surface view to us. This will emit a surfaceCreated and surfaceChanged call that
132        // will update our screen width and height.
133        mSurfaceView.unbind();
134        mSurfaceView.bindToListener(this, context.getGLEnvironment());
135    }
136
137    @Override
138    public void process(FilterContext context) {
139        // Make sure we are bound to a surface before rendering
140        if (!mIsBound) {
141            Log.w("SurfaceRenderFilter",
142                  this + ": Ignoring frame as there is no surface to render to!");
143            return;
144        }
145
146        if (mLogVerbose) Log.v(TAG, "Starting frame processing");
147
148        GLEnvironment glEnv = mSurfaceView.getGLEnv();
149        if (glEnv != context.getGLEnvironment()) {
150            throw new RuntimeException("Surface created under different GLEnvironment!");
151        }
152
153
154        // Get input frame
155        Frame input = pullInput("frame");
156        boolean createdFrame = false;
157
158        float currentAspectRatio = (float)input.getFormat().getWidth() / input.getFormat().getHeight();
159        if (currentAspectRatio != mAspectRatio) {
160            if (mLogVerbose) Log.v(TAG, "New aspect ratio: " + currentAspectRatio +", previously: " + mAspectRatio);
161            mAspectRatio = currentAspectRatio;
162            updateTargetRect();
163        }
164
165        // See if we need to copy to GPU
166        Frame gpuFrame = null;
167        if (mLogVerbose) Log.v("SurfaceRenderFilter", "Got input format: " + input.getFormat());
168        int target = input.getFormat().getTarget();
169        if (target != FrameFormat.TARGET_GPU) {
170            gpuFrame = context.getFrameManager().duplicateFrameToTarget(input,
171                                                                        FrameFormat.TARGET_GPU);
172            createdFrame = true;
173        } else {
174            gpuFrame = input;
175        }
176
177        // Activate our surface
178        glEnv.activateSurfaceWithId(mSurfaceView.getSurfaceId());
179
180        // Process
181        mProgram.process(gpuFrame, mScreen);
182
183        // And swap buffers
184        glEnv.swapBuffers();
185
186        if (createdFrame) {
187            gpuFrame.release();
188        }
189    }
190
191    @Override
192    public void fieldPortValueUpdated(String name, FilterContext context) {
193        updateTargetRect();
194    }
195
196    @Override
197    public void close(FilterContext context) {
198        mSurfaceView.unbind();
199    }
200
201    @Override
202    public void tearDown(FilterContext context) {
203        if (mScreen != null) {
204            mScreen.release();
205        }
206    }
207
208    @Override
209    public synchronized void surfaceCreated(SurfaceHolder holder) {
210        mIsBound = true;
211    }
212
213    @Override
214    public synchronized void surfaceChanged(SurfaceHolder holder,
215                                            int format,
216                                            int width,
217                                            int height) {
218        // If the screen is null, we do not care about surface changes (yet). Once we have a
219        // screen object, we need to keep track of these changes.
220        if (mScreen != null) {
221            mScreenWidth = width;
222            mScreenHeight = height;
223            mScreen.setViewport(0, 0, mScreenWidth, mScreenHeight);
224            updateTargetRect();
225        }
226    }
227
228    @Override
229    public synchronized void surfaceDestroyed(SurfaceHolder holder) {
230        mIsBound = false;
231    }
232
233    private void updateTargetRect() {
234        if (mScreenWidth > 0 && mScreenHeight > 0 && mProgram != null) {
235            float screenAspectRatio = (float)mScreenWidth / mScreenHeight;
236            float relativeAspectRatio = screenAspectRatio / mAspectRatio;
237
238            switch (mRenderMode) {
239                case RENDERMODE_STRETCH:
240                    mProgram.setTargetRect(0, 0, 1, 1);
241                    break;
242                case RENDERMODE_FIT:
243                    if (relativeAspectRatio > 1.0f) {
244                        // Screen is wider than the camera, scale down X
245                        mProgram.setTargetRect(0.5f - 0.5f / relativeAspectRatio, 0.0f,
246                                               1.0f / relativeAspectRatio, 1.0f);
247                    } else {
248                        // Screen is taller than the camera, scale down Y
249                        mProgram.setTargetRect(0.0f, 0.5f - 0.5f * relativeAspectRatio,
250                                               1.0f, relativeAspectRatio);
251                    }
252                    break;
253                case RENDERMODE_FILL_CROP:
254                    if (relativeAspectRatio > 1) {
255                        // Screen is wider than the camera, crop in Y
256                        mProgram.setTargetRect(0.0f, 0.5f - 0.5f * relativeAspectRatio,
257                                               1.0f, relativeAspectRatio);
258                    } else {
259                        // Screen is taller than the camera, crop in X
260                        mProgram.setTargetRect(0.5f - 0.5f / relativeAspectRatio, 0.0f,
261                                               1.0f / relativeAspectRatio, 1.0f);
262                    }
263                    break;
264            }
265        }
266    }
267}
268