| /* |
| * Copyright (C) 2022 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 android.graphics; |
| |
| import android.annotation.FloatRange; |
| import android.annotation.IntDef; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.graphics.ColorSpace.Named; |
| import android.hardware.HardwareBuffer; |
| import android.hardware.SyncFence; |
| import android.view.SurfaceControl; |
| |
| import libcore.util.NativeAllocationRegistry; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.concurrent.Executor; |
| import java.util.function.Consumer; |
| |
| /** |
| * <p>Creates an instance of a hardware-accelerated renderer. This is used to render a scene built |
| * from {@link RenderNode}s to an output {@link HardwareBuffer}. There can be as many |
| * HardwareBufferRenderer instances as desired.</p> |
| * |
| * <h3>Resources & lifecycle</h3> |
| * |
| * <p>All HardwareBufferRenderer and {@link HardwareRenderer} instances share a common render |
| * thread. Therefore HardwareBufferRenderer will share common resources and GPU utilization with |
| * hardware accelerated rendering initiated by the UI thread of an application. |
| * The render thread contains the GPU context & resources necessary to do GPU-accelerated |
| * rendering. As such, the first HardwareBufferRenderer created comes with the cost of also creating |
| * the associated GPU contexts, however each incremental HardwareBufferRenderer thereafter is fairly |
| * cheap. The expected usage is to have a HardwareBufferRenderer instance for every active {@link |
| * HardwareBuffer}.</p> |
| * |
| * This is useful in situations where a scene built with {@link RenderNode}s can be consumed |
| * directly by the system compositor through |
| * {@link SurfaceControl.Transaction#setBuffer(SurfaceControl, HardwareBuffer)}. |
| * |
| * HardwareBufferRenderer will never clear contents before each draw invocation so previous contents |
| * in the {@link HardwareBuffer} target will be preserved across renders. |
| */ |
| public class HardwareBufferRenderer implements AutoCloseable { |
| |
| private static final ColorSpace DEFAULT_COLORSPACE = ColorSpace.get(Named.SRGB); |
| |
| private static class HardwareBufferRendererHolder { |
| public static final NativeAllocationRegistry REGISTRY = |
| NativeAllocationRegistry.createMalloced( |
| HardwareBufferRenderer.class.getClassLoader(), nGetFinalizer()); |
| } |
| |
| private final HardwareBuffer mHardwareBuffer; |
| private final RenderRequest mRenderRequest; |
| private final RenderNode mRootNode; |
| private final Runnable mCleaner; |
| |
| private long mProxy; |
| |
| /** |
| * Creates a new instance of {@link HardwareBufferRenderer} with the provided {@link |
| * HardwareBuffer} as the output of the rendered scene. |
| */ |
| public HardwareBufferRenderer(@NonNull HardwareBuffer buffer) { |
| RenderNode rootNode = RenderNode.adopt(nCreateRootRenderNode()); |
| rootNode.setClipToBounds(false); |
| mProxy = nCreateHardwareBufferRenderer(buffer, rootNode.mNativeRenderNode); |
| mCleaner = HardwareBufferRendererHolder.REGISTRY.registerNativeAllocation(this, mProxy); |
| mRenderRequest = new RenderRequest(); |
| mRootNode = rootNode; |
| mHardwareBuffer = buffer; |
| } |
| |
| /** |
| * Sets the content root to render. It is not necessary to call this whenever the content |
| * recording changes. Any mutations to the RenderNode content, or any of the RenderNodes |
| * contained within the content node, will be applied whenever a new {@link RenderRequest} is |
| * issued via {@link #obtainRenderRequest()} and {@link RenderRequest#draw(Executor, |
| * Consumer)}. |
| * |
| * @param content The content to set as the root RenderNode. If null the content root is removed |
| * and the renderer will draw nothing. |
| */ |
| public void setContentRoot(@Nullable RenderNode content) { |
| RecordingCanvas canvas = mRootNode.beginRecording(); |
| if (content != null) { |
| canvas.drawRenderNode(content); |
| } |
| mRootNode.endRecording(); |
| } |
| |
| /** |
| * Returns a {@link RenderRequest} that can be used to render into the provided {@link |
| * HardwareBuffer}. This is used to synchronize the RenderNode content provided by {@link |
| * #setContentRoot(RenderNode)}. |
| * |
| * @return An instance of {@link RenderRequest}. The instance may be reused for every frame, so |
| * the caller should not hold onto it for longer than a single render request. |
| */ |
| @NonNull |
| public RenderRequest obtainRenderRequest() { |
| mRenderRequest.reset(); |
| return mRenderRequest; |
| } |
| |
| /** |
| * Returns if the {@link HardwareBufferRenderer} has already been closed. That is |
| * {@link HardwareBufferRenderer#close()} has been invoked. |
| * @return True if the {@link HardwareBufferRenderer} has been closed, false otherwise. |
| */ |
| public boolean isClosed() { |
| return mProxy == 0L; |
| } |
| |
| /** |
| * Releases the resources associated with this {@link HardwareBufferRenderer} instance. **Note** |
| * this does not call {@link HardwareBuffer#close()} on the provided {@link HardwareBuffer} |
| * instance |
| */ |
| @Override |
| public void close() { |
| // Note we explicitly call this only here to clean-up potential animator state |
| // This is not done as part of the NativeAllocationRegistry as it would invoke animator |
| // callbacks on the wrong thread |
| nDestroyRootRenderNode(mRootNode.mNativeRenderNode); |
| if (mProxy != 0L) { |
| mCleaner.run(); |
| mProxy = 0L; |
| } |
| } |
| |
| /** |
| * Sets the center of the light source. The light source point controls the directionality and |
| * shape of shadows rendered by RenderNode Z & elevation. |
| * |
| * <p>The light source should be setup both as part of initial configuration, and whenever |
| * the window moves to ensure the light source stays anchored in display space instead of in |
| * window space. |
| * |
| * <p>This must be set at least once along with {@link #setLightSourceAlpha(float, float)} |
| * before shadows will work. |
| * |
| * @param lightX The X position of the light source. If unsure, a reasonable default |
| * is 'displayWidth / 2f - windowLeft'. |
| * @param lightY The Y position of the light source. If unsure, a reasonable default |
| * is '0 - windowTop' |
| * @param lightZ The Z position of the light source. Must be >= 0. If unsure, a reasonable |
| * default is 600dp. |
| * @param lightRadius The radius of the light source. Smaller radius will have sharper edges, |
| * larger radius will have softer shadows. If unsure, a reasonable default is 800 dp. |
| */ |
| public void setLightSourceGeometry( |
| float lightX, |
| float lightY, |
| @FloatRange(from = 0f) float lightZ, |
| @FloatRange(from = 0f) float lightRadius |
| ) { |
| validateFinite(lightX, "lightX"); |
| validateFinite(lightY, "lightY"); |
| validatePositive(lightZ, "lightZ"); |
| validatePositive(lightRadius, "lightRadius"); |
| nSetLightGeometry(mProxy, lightX, lightY, lightZ, lightRadius); |
| } |
| |
| /** |
| * Configures the ambient & spot shadow alphas. This is the alpha used when the shadow has max |
| * alpha, and ramps down from the values provided to zero. |
| * |
| * <p>These values are typically provided by the current theme, see |
| * {@link android.R.attr#spotShadowAlpha} and {@link android.R.attr#ambientShadowAlpha}. |
| * |
| * <p>This must be set at least once along with |
| * {@link #setLightSourceGeometry(float, float, float, float)} before shadows will work. |
| * |
| * @param ambientShadowAlpha The alpha for the ambient shadow. If unsure, a reasonable default |
| * is 0.039f. |
| * @param spotShadowAlpha The alpha for the spot shadow. If unsure, a reasonable default is |
| * 0.19f. |
| */ |
| public void setLightSourceAlpha(@FloatRange(from = 0.0f, to = 1.0f) float ambientShadowAlpha, |
| @FloatRange(from = 0.0f, to = 1.0f) float spotShadowAlpha) { |
| validateAlpha(ambientShadowAlpha, "ambientShadowAlpha"); |
| validateAlpha(spotShadowAlpha, "spotShadowAlpha"); |
| nSetLightAlpha(mProxy, ambientShadowAlpha, spotShadowAlpha); |
| } |
| |
| /** |
| * Class that contains data regarding the result of the render request. |
| * Consumers are to wait on the provided {@link SyncFence} before consuming the HardwareBuffer |
| * provided to {@link HardwareBufferRenderer} as well as verify that the status returned by |
| * {@link RenderResult#getStatus()} returns {@link RenderResult#SUCCESS}. |
| */ |
| public static final class RenderResult { |
| |
| /** |
| * Render request was completed successfully |
| */ |
| public static final int SUCCESS = 0; |
| |
| /** |
| * Render request failed with an unknown error |
| */ |
| public static final int ERROR_UNKNOWN = 1; |
| |
| /** @hide **/ |
| @IntDef(value = {SUCCESS, ERROR_UNKNOWN}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface RenderResultStatus{} |
| |
| private final SyncFence mFence; |
| private final int mResultStatus; |
| |
| private RenderResult(@NonNull SyncFence fence, @RenderResultStatus int resultStatus) { |
| mFence = fence; |
| mResultStatus = resultStatus; |
| } |
| |
| @NonNull |
| public SyncFence getFence() { |
| return mFence; |
| } |
| |
| @RenderResultStatus |
| public int getStatus() { |
| return mResultStatus; |
| } |
| } |
| |
| /** |
| * Sets the parameters that can be used to control a render request for a {@link |
| * HardwareBufferRenderer}. This is not thread-safe and must not be held on to for longer than a |
| * single request. |
| */ |
| public final class RenderRequest { |
| |
| private ColorSpace mColorSpace = DEFAULT_COLORSPACE; |
| private int mTransform = SurfaceControl.BUFFER_TRANSFORM_IDENTITY; |
| |
| private RenderRequest() { } |
| |
| /** |
| * Syncs the RenderNode tree to the render thread and requests content to be drawn. This |
| * {@link RenderRequest} instance should no longer be used after calling this method. The |
| * system internally may reuse instances of {@link RenderRequest} to reduce allocation |
| * churn. |
| * |
| * @param executor Executor used to deliver callbacks |
| * @param renderCallback Callback invoked when rendering is complete. This includes a |
| * {@link RenderResult} that provides a {@link SyncFence} that should be waited upon for |
| * completion before consuming the rendered output in the provided {@link HardwareBuffer} |
| * instance. |
| * |
| * @throws IllegalStateException if attempt to draw is made when |
| * {@link HardwareBufferRenderer#isClosed()} returns true |
| */ |
| public void draw( |
| @NonNull Executor executor, |
| @NonNull Consumer<RenderResult> renderCallback |
| ) { |
| Consumer<RenderResult> wrapped = consumable -> executor.execute( |
| () -> renderCallback.accept(consumable)); |
| if (!isClosed()) { |
| int renderWidth; |
| int renderHeight; |
| if (mTransform == SurfaceControl.BUFFER_TRANSFORM_ROTATE_90 |
| || mTransform == SurfaceControl.BUFFER_TRANSFORM_ROTATE_270) { |
| renderWidth = mHardwareBuffer.getHeight(); |
| renderHeight = mHardwareBuffer.getWidth(); |
| } else { |
| renderWidth = mHardwareBuffer.getWidth(); |
| renderHeight = mHardwareBuffer.getHeight(); |
| } |
| |
| nRender( |
| mProxy, |
| mTransform, |
| renderWidth, |
| renderHeight, |
| mColorSpace.getNativeInstance(), |
| wrapped); |
| } else { |
| throw new IllegalStateException("Attempt to draw with a HardwareBufferRenderer " |
| + "instance that has already been closed"); |
| } |
| } |
| |
| private void reset() { |
| mColorSpace = DEFAULT_COLORSPACE; |
| mTransform = SurfaceControl.BUFFER_TRANSFORM_IDENTITY; |
| } |
| |
| /** |
| * Configures the color space which the content should be rendered in. This affects |
| * how the framework will interpret the color at each pixel. The color space provided here |
| * must be non-null, RGB based and leverage an ICC parametric curve. The min/max values |
| * of the components should not reduce the numerical range compared to the previously |
| * assigned color space. If left unspecified, the default color space of SRGB will be used. |
| * |
| * @param colorSpace The color space the content should be rendered in. If null is provided |
| * the default of SRGB will be used. |
| */ |
| @NonNull |
| public RenderRequest setColorSpace(@Nullable ColorSpace colorSpace) { |
| if (colorSpace == null) { |
| mColorSpace = DEFAULT_COLORSPACE; |
| } else { |
| mColorSpace = colorSpace; |
| } |
| return this; |
| } |
| |
| /** |
| * Specifies a transform to be applied before content is rendered. This is useful |
| * for pre-rotating content for the current display orientation to increase performance |
| * of displaying the associated buffer. This transformation will also adjust the light |
| * source position for the specified rotation. |
| * @see SurfaceControl.Transaction#setBufferTransform(SurfaceControl, int) |
| */ |
| @NonNull |
| public RenderRequest setBufferTransform( |
| @SurfaceControl.BufferTransform int bufferTransform) { |
| boolean validTransform = bufferTransform == SurfaceControl.BUFFER_TRANSFORM_IDENTITY |
| || bufferTransform == SurfaceControl.BUFFER_TRANSFORM_ROTATE_90 |
| || bufferTransform == SurfaceControl.BUFFER_TRANSFORM_ROTATE_180 |
| || bufferTransform == SurfaceControl.BUFFER_TRANSFORM_ROTATE_270; |
| if (validTransform) { |
| mTransform = bufferTransform; |
| } else { |
| throw new IllegalArgumentException("Invalid transform provided, must be one of" |
| + "the SurfaceControl.BufferTransform values"); |
| } |
| return this; |
| } |
| } |
| |
| /** |
| * @hide |
| */ |
| /* package */ |
| static native int nRender(long renderer, int transform, int width, int height, long colorSpace, |
| Consumer<RenderResult> callback); |
| |
| private static native long nCreateRootRenderNode(); |
| |
| private static native void nDestroyRootRenderNode(long rootRenderNode); |
| |
| private static native long nCreateHardwareBufferRenderer(HardwareBuffer buffer, |
| long rootRenderNode); |
| |
| private static native void nSetLightGeometry(long bufferRenderer, float lightX, float lightY, |
| float lightZ, float radius); |
| |
| private static native void nSetLightAlpha(long nativeProxy, float ambientShadowAlpha, |
| float spotShadowAlpha); |
| |
| private static native long nGetFinalizer(); |
| |
| // Called by native |
| private static void invokeRenderCallback( |
| @NonNull Consumer<RenderResult> callback, |
| int fd, |
| int status |
| ) { |
| callback.accept(new RenderResult(SyncFence.adopt(fd), status)); |
| } |
| |
| private static void validateAlpha(float alpha, String argumentName) { |
| if (!(alpha >= 0.0f && alpha <= 1.0f)) { |
| throw new IllegalArgumentException(argumentName + " must be a valid alpha, " |
| + alpha + " is not in the range of 0.0f to 1.0f"); |
| } |
| } |
| |
| private static void validateFinite(float f, String argumentName) { |
| if (!Float.isFinite(f)) { |
| throw new IllegalArgumentException(argumentName + " must be finite, given=" + f); |
| } |
| } |
| |
| private static void validatePositive(float f, String argumentName) { |
| if (!(Float.isFinite(f) && f >= 0.0f)) { |
| throw new IllegalArgumentException(argumentName |
| + " must be a finite positive, given=" + f); |
| } |
| } |
| } |