/* * 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; /** *

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.

* *

Resources & lifecycle

* *

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

* * 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. * *

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. * *

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. * *

These values are typically provided by the current theme, see * {@link android.R.attr#spotShadowAlpha} and {@link android.R.attr#ambientShadowAlpha}. * *

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 renderCallback ) { Consumer 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 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 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); } } }