| /* |
| * Copyright 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. |
| */ |
| |
| #include "GuestFrameComposer.h" |
| |
| #include <android-base/parseint.h> |
| #include <android-base/properties.h> |
| #include <android-base/strings.h> |
| #include <android/hardware/graphics/common/1.0/types.h> |
| #include <device_config_shared.h> |
| #include <drm_fourcc.h> |
| #include <libyuv.h> |
| #include <sync/sync.h> |
| #include <ui/GraphicBuffer.h> |
| #include <ui/GraphicBufferAllocator.h> |
| #include <ui/GraphicBufferMapper.h> |
| |
| #include "Display.h" |
| #include "Drm.h" |
| #include "Layer.h" |
| |
| namespace aidl::android::hardware::graphics::composer3::impl { |
| namespace { |
| |
| using ::android::hardware::graphics::common::V1_0::ColorTransform; |
| |
| uint64_t AlignToPower2(uint64_t val, uint8_t align_log) { |
| uint64_t align = 1ULL << align_log; |
| return ((val + (align - 1)) / align) * align; |
| } |
| |
| bool LayerNeedsScaling(const Layer& layer) { |
| common::Rect crop = layer.getSourceCropInt(); |
| common::Rect frame = layer.getDisplayFrame(); |
| |
| int fromW = crop.right - crop.left; |
| int fromH = crop.bottom - crop.top; |
| int toW = frame.right - frame.left; |
| int toH = frame.bottom - frame.top; |
| |
| bool not_rot_scale = fromW != toW || fromH != toH; |
| bool rot_scale = fromW != toH || fromH != toW; |
| |
| bool needs_rot = static_cast<int32_t>(layer.getTransform()) & |
| static_cast<int32_t>(common::Transform::ROT_90); |
| |
| return needs_rot ? rot_scale : not_rot_scale; |
| } |
| |
| bool LayerNeedsBlending(const Layer& layer) { |
| return layer.getBlendMode() != common::BlendMode::NONE; |
| } |
| |
| bool LayerNeedsAttenuation(const Layer& layer) { |
| return layer.getBlendMode() == common::BlendMode::COVERAGE; |
| } |
| |
| struct BufferSpec; |
| typedef int (*ConverterFunction)(const BufferSpec& src, const BufferSpec& dst, |
| bool v_flip); |
| int DoCopy(const BufferSpec& src, const BufferSpec& dst, bool vFlip); |
| int ConvertFromRGB565(const BufferSpec& src, const BufferSpec& dst, bool vFlip); |
| int ConvertFromYV12(const BufferSpec& src, const BufferSpec& dst, bool vFlip); |
| |
| ConverterFunction GetConverterForDrmFormat(uint32_t drmFormat) { |
| switch (drmFormat) { |
| case DRM_FORMAT_ABGR8888: |
| case DRM_FORMAT_XBGR8888: |
| return &DoCopy; |
| case DRM_FORMAT_RGB565: |
| return &ConvertFromRGB565; |
| case DRM_FORMAT_YVU420: |
| return &ConvertFromYV12; |
| } |
| DEBUG_LOG("Unsupported drm format: %d(%s), returning null converter", |
| drmFormat, GetDrmFormatString(drmFormat)); |
| return nullptr; |
| } |
| |
| bool IsDrmFormatSupported(uint32_t drmFormat) { |
| return GetConverterForDrmFormat(drmFormat) != nullptr; |
| } |
| |
| // Libyuv's convert functions only allow the combination of any rotation |
| // (multiple of 90 degrees) and a vertical flip, but not horizontal flips. |
| // Surfaceflinger's transformations are expressed in terms of a vertical flip, |
| // a horizontal flip and/or a single 90 degrees clockwise rotation (see |
| // NATIVE_WINDOW_TRANSFORM_HINT documentation on system/window.h for more |
| // insight). The following code allows to turn a horizontal flip into a 180 |
| // degrees rotation and a vertical flip. |
| libyuv::RotationMode GetRotationFromTransform(common::Transform transform) { |
| uint32_t rotation = 0; |
| rotation += (static_cast<int32_t>(transform) & |
| static_cast<int32_t>(common::Transform::ROT_90)) |
| ? 1 |
| : 0; // 1 * ROT90 bit |
| rotation += (static_cast<int32_t>(transform) & |
| static_cast<int32_t>(common::Transform::FLIP_H)) |
| ? 2 |
| : 0; // 2 * VFLIP bit |
| return static_cast<libyuv::RotationMode>(90 * rotation); |
| } |
| |
| bool GetVFlipFromTransform(common::Transform transform) { |
| // vertical flip xor horizontal flip |
| bool hasVFlip = static_cast<int32_t>(transform) & |
| static_cast<int32_t>(common::Transform::FLIP_V); |
| bool hasHFlip = static_cast<int32_t>(transform) & |
| static_cast<int32_t>(common::Transform::FLIP_H); |
| return hasVFlip ^ hasHFlip; |
| } |
| |
| struct BufferSpec { |
| uint8_t* buffer; |
| std::optional<android_ycbcr> buffer_ycbcr; |
| int width; |
| int height; |
| int cropX; |
| int cropY; |
| int cropWidth; |
| int cropHeight; |
| uint32_t drmFormat; |
| int strideBytes; |
| int sampleBytes; |
| |
| BufferSpec(uint8_t* buffer, std::optional<android_ycbcr> buffer_ycbcr, |
| int width, int height, int cropX, int cropY, int cropWidth, |
| int cropHeight, uint32_t drmFormat, int strideBytes, |
| int sampleBytes) |
| : buffer(buffer), |
| buffer_ycbcr(buffer_ycbcr), |
| width(width), |
| height(height), |
| cropX(cropX), |
| cropY(cropY), |
| cropWidth(cropWidth), |
| cropHeight(cropHeight), |
| drmFormat(drmFormat), |
| strideBytes(strideBytes), |
| sampleBytes(sampleBytes) {} |
| |
| BufferSpec(uint8_t* buffer, int width, int height, int strideBytes) |
| : BufferSpec(buffer, |
| /*buffer_ycbcr=*/std::nullopt, width, height, |
| /*cropX=*/0, |
| /*cropY=*/0, |
| /*cropWidth=*/width, |
| /*cropHeight=*/height, |
| /*drmFormat=*/DRM_FORMAT_ABGR8888, strideBytes, |
| /*sampleBytes=*/4) {} |
| }; |
| |
| int ConvertFromRGB565(const BufferSpec& src, const BufferSpec& dst, |
| bool vFlip) { |
| ATRACE_CALL(); |
| |
| // Point to the upper left corner of the crop rectangle |
| uint8_t* srcBuffer = |
| src.buffer + src.cropY * src.strideBytes + src.cropX * src.sampleBytes; |
| uint8_t* dstBuffer = |
| dst.buffer + dst.cropY * dst.strideBytes + dst.cropX * dst.sampleBytes; |
| |
| int width = src.cropWidth; |
| int height = src.cropHeight; |
| if (vFlip) { |
| height = -height; |
| } |
| |
| return libyuv::RGB565ToARGB(srcBuffer, src.strideBytes, // |
| dstBuffer, dst.strideBytes, // |
| width, height); |
| } |
| |
| int ConvertFromYV12(const BufferSpec& src, const BufferSpec& dst, bool vFlip) { |
| ATRACE_CALL(); |
| |
| // The following calculation of plane offsets and alignments are based on |
| // swiftshader's Sampler::setTextureLevel() implementation |
| // (Renderer/Sampler.cpp:225) |
| |
| auto& srcBufferYCbCrOpt = src.buffer_ycbcr; |
| if (!srcBufferYCbCrOpt) { |
| ALOGE("%s called on non ycbcr buffer", __FUNCTION__); |
| return -1; |
| } |
| auto& srcBufferYCbCr = *srcBufferYCbCrOpt; |
| |
| // The libyuv::I420ToARGB() function is for tri-planar. |
| if (srcBufferYCbCr.chroma_step != 1) { |
| ALOGE("%s called with bad chroma step", __FUNCTION__); |
| return -1; |
| } |
| |
| uint8_t* srcY = reinterpret_cast<uint8_t*>(srcBufferYCbCr.y); |
| int strideY = srcBufferYCbCr.ystride; |
| uint8_t* srcU = reinterpret_cast<uint8_t*>(srcBufferYCbCr.cb); |
| int strideU = srcBufferYCbCr.cstride; |
| uint8_t* srcV = reinterpret_cast<uint8_t*>(srcBufferYCbCr.cr); |
| int strideV = srcBufferYCbCr.cstride; |
| |
| // Adjust for crop |
| srcY += src.cropY * strideY + src.cropX; |
| srcV += (src.cropY / 2) * strideV + (src.cropX / 2); |
| srcU += (src.cropY / 2) * strideU + (src.cropX / 2); |
| uint8_t* dstBuffer = |
| dst.buffer + dst.cropY * dst.strideBytes + dst.cropX * dst.sampleBytes; |
| |
| int width = dst.cropWidth; |
| int height = dst.cropHeight; |
| |
| if (vFlip) { |
| height = -height; |
| } |
| |
| // YV12 is the same as I420, with the U and V planes swapped |
| return libyuv::I420ToARGB(srcY, strideY, srcV, strideV, srcU, strideU, |
| dstBuffer, dst.strideBytes, width, height); |
| } |
| |
| int DoConversion(const BufferSpec& src, const BufferSpec& dst, bool v_flip) { |
| ConverterFunction func = GetConverterForDrmFormat(src.drmFormat); |
| if (!func) { |
| // GetConverterForDrmFormat should've logged the issue for us. |
| return -1; |
| } |
| return func(src, dst, v_flip); |
| } |
| |
| int DoCopy(const BufferSpec& src, const BufferSpec& dst, bool v_flip) { |
| ATRACE_CALL(); |
| |
| // Point to the upper left corner of the crop rectangle |
| uint8_t* srcBuffer = |
| src.buffer + src.cropY * src.strideBytes + src.cropX * src.sampleBytes; |
| uint8_t* dstBuffer = |
| dst.buffer + dst.cropY * dst.strideBytes + dst.cropX * dst.sampleBytes; |
| int width = src.cropWidth; |
| int height = src.cropHeight; |
| |
| if (v_flip) { |
| height = -height; |
| } |
| |
| // HAL formats are named based on the order of the pixel components on the |
| // byte stream, while libyuv formats are named based on the order of those |
| // pixel components in an integer written from left to right. So |
| // libyuv::FOURCC_ARGB is equivalent to HAL_PIXEL_FORMAT_BGRA_8888. |
| auto ret = libyuv::ARGBCopy(srcBuffer, src.strideBytes, dstBuffer, |
| dst.strideBytes, width, height); |
| return ret; |
| } |
| |
| int DoRotation(const BufferSpec& src, const BufferSpec& dst, |
| libyuv::RotationMode rotation, bool v_flip) { |
| ATRACE_CALL(); |
| |
| // Point to the upper left corner of the crop rectangles |
| uint8_t* srcBuffer = |
| src.buffer + src.cropY * src.strideBytes + src.cropX * src.sampleBytes; |
| uint8_t* dstBuffer = |
| dst.buffer + dst.cropY * dst.strideBytes + dst.cropX * dst.sampleBytes; |
| int width = src.cropWidth; |
| int height = src.cropHeight; |
| |
| if (v_flip) { |
| height = -height; |
| } |
| |
| return libyuv::ARGBRotate(srcBuffer, src.strideBytes, dstBuffer, |
| dst.strideBytes, width, height, rotation); |
| } |
| |
| int DoScaling(const BufferSpec& src, const BufferSpec& dst, bool v_flip) { |
| ATRACE_CALL(); |
| |
| // Point to the upper left corner of the crop rectangles |
| uint8_t* srcBuffer = |
| src.buffer + src.cropY * src.strideBytes + src.cropX * src.sampleBytes; |
| uint8_t* dstBuffer = |
| dst.buffer + dst.cropY * dst.strideBytes + dst.cropX * dst.sampleBytes; |
| int srcWidth = src.cropWidth; |
| int srcHeight = src.cropHeight; |
| int dstWidth = dst.cropWidth; |
| int dstHeight = dst.cropHeight; |
| |
| if (v_flip) { |
| srcHeight = -srcHeight; |
| } |
| |
| return libyuv::ARGBScale(srcBuffer, src.strideBytes, srcWidth, srcHeight, |
| dstBuffer, dst.strideBytes, dstWidth, dstHeight, |
| libyuv::kFilterBilinear); |
| } |
| |
| int DoAttenuation(const BufferSpec& src, const BufferSpec& dst, bool v_flip) { |
| ATRACE_CALL(); |
| |
| // Point to the upper left corner of the crop rectangles |
| uint8_t* srcBuffer = |
| src.buffer + src.cropY * src.strideBytes + src.cropX * src.sampleBytes; |
| uint8_t* dstBuffer = |
| dst.buffer + dst.cropY * dst.strideBytes + dst.cropX * dst.sampleBytes; |
| int width = dst.cropWidth; |
| int height = dst.cropHeight; |
| |
| if (v_flip) { |
| height = -height; |
| } |
| |
| return libyuv::ARGBAttenuate(srcBuffer, src.strideBytes, dstBuffer, |
| dst.strideBytes, width, height); |
| } |
| |
| int DoBlending(const BufferSpec& src, const BufferSpec& dst, bool v_flip) { |
| ATRACE_CALL(); |
| |
| // Point to the upper left corner of the crop rectangles |
| uint8_t* srcBuffer = |
| src.buffer + src.cropY * src.strideBytes + src.cropX * src.sampleBytes; |
| uint8_t* dstBuffer = |
| dst.buffer + dst.cropY * dst.strideBytes + dst.cropX * dst.sampleBytes; |
| int width = dst.cropWidth; |
| int height = dst.cropHeight; |
| |
| if (v_flip) { |
| height = -height; |
| } |
| |
| // libyuv's ARGB format is hwcomposer's BGRA format, since blending only cares |
| // for the position of alpha in the pixel and not the position of the colors |
| // this function is perfectly usable. |
| return libyuv::ARGBBlend(srcBuffer, src.strideBytes, dstBuffer, |
| dst.strideBytes, dstBuffer, dst.strideBytes, width, |
| height); |
| } |
| |
| std::optional<BufferSpec> GetBufferSpec(GrallocBuffer& buffer, |
| GrallocBufferView& bufferView, |
| const common::Rect& bufferCrop) { |
| auto bufferFormatOpt = buffer.GetDrmFormat(); |
| if (!bufferFormatOpt) { |
| ALOGE("Failed to get gralloc buffer format."); |
| return std::nullopt; |
| } |
| uint32_t bufferFormat = *bufferFormatOpt; |
| |
| auto bufferWidthOpt = buffer.GetWidth(); |
| if (!bufferWidthOpt) { |
| ALOGE("Failed to get gralloc buffer width."); |
| return std::nullopt; |
| } |
| uint32_t bufferWidth = *bufferWidthOpt; |
| |
| auto bufferHeightOpt = buffer.GetHeight(); |
| if (!bufferHeightOpt) { |
| ALOGE("Failed to get gralloc buffer height."); |
| return std::nullopt; |
| } |
| uint32_t bufferHeight = *bufferHeightOpt; |
| |
| uint8_t* bufferData = nullptr; |
| uint32_t bufferStrideBytes = 0; |
| std::optional<android_ycbcr> bufferYCbCrData; |
| |
| if (bufferFormat == DRM_FORMAT_NV12 || bufferFormat == DRM_FORMAT_NV21 || |
| bufferFormat == DRM_FORMAT_YVU420) { |
| bufferYCbCrData = bufferView.GetYCbCr(); |
| if (!bufferYCbCrData) { |
| ALOGE("%s failed to get raw ycbcr from view.", __FUNCTION__); |
| return std::nullopt; |
| } |
| } else { |
| auto bufferDataOpt = bufferView.Get(); |
| if (!bufferDataOpt) { |
| ALOGE("%s failed to lock gralloc buffer.", __FUNCTION__); |
| return std::nullopt; |
| } |
| bufferData = reinterpret_cast<uint8_t*>(*bufferDataOpt); |
| |
| auto bufferStrideBytesOpt = buffer.GetMonoPlanarStrideBytes(); |
| if (!bufferStrideBytesOpt) { |
| ALOGE("%s failed to get plane stride.", __FUNCTION__); |
| return std::nullopt; |
| } |
| bufferStrideBytes = *bufferStrideBytesOpt; |
| } |
| |
| return BufferSpec(bufferData, bufferYCbCrData, bufferWidth, bufferHeight, |
| bufferCrop.left, bufferCrop.top, |
| bufferCrop.right - bufferCrop.left, |
| bufferCrop.bottom - bufferCrop.top, bufferFormat, |
| bufferStrideBytes, GetDrmFormatBytesPerPixel(bufferFormat)); |
| } |
| |
| } // namespace |
| |
| HWC3::Error GuestFrameComposer::init() { |
| DEBUG_LOG("%s", __FUNCTION__); |
| |
| HWC3::Error error = mDrmClient.init(); |
| if (error != HWC3::Error::None) { |
| ALOGE("%s: failed to initialize DrmClient", __FUNCTION__); |
| return error; |
| } |
| |
| return HWC3::Error::None; |
| } |
| |
| HWC3::Error GuestFrameComposer::registerOnHotplugCallback( |
| const HotplugCallback& cb) { |
| return mDrmClient.registerOnHotplugCallback(cb); |
| return HWC3::Error::None; |
| } |
| |
| HWC3::Error GuestFrameComposer::unregisterOnHotplugCallback() { |
| return mDrmClient.unregisterOnHotplugCallback(); |
| } |
| |
| HWC3::Error GuestFrameComposer::onDisplayCreate(Display* display) { |
| int64_t displayId = display->getId(); |
| int32_t displayConfigId; |
| int32_t displayWidth; |
| int32_t displayHeight; |
| |
| HWC3::Error error = display->getActiveConfig(&displayConfigId); |
| if (error != HWC3::Error::None) { |
| ALOGE("%s: display:%" PRIu64 " has no active config", __FUNCTION__, |
| displayId); |
| return error; |
| } |
| |
| error = display->getDisplayAttribute(displayConfigId, DisplayAttribute::WIDTH, |
| &displayWidth); |
| if (error != HWC3::Error::None) { |
| ALOGE("%s: display:%" PRIu64 " failed to get width", __FUNCTION__, |
| displayId); |
| return error; |
| } |
| |
| error = display->getDisplayAttribute( |
| displayConfigId, DisplayAttribute::HEIGHT, &displayHeight); |
| if (error != HWC3::Error::None) { |
| ALOGE("%s: display:%" PRIu64 " failed to get height", __FUNCTION__, |
| displayId); |
| return error; |
| } |
| |
| auto it = mDisplayInfos.find(displayId); |
| if (it != mDisplayInfos.end()) { |
| ALOGE("%s: display:%" PRIu64 " already created?", __FUNCTION__, displayId); |
| } |
| |
| DisplayInfo& displayInfo = mDisplayInfos[displayId]; |
| |
| uint32_t bufferStride; |
| buffer_handle_t bufferHandle; |
| |
| auto status = ::android::GraphicBufferAllocator::get().allocate( |
| displayWidth, // |
| displayHeight, // |
| ::android::PIXEL_FORMAT_RGBA_8888, // |
| /*layerCount=*/1, // |
| ::android::GraphicBuffer::USAGE_HW_COMPOSER | |
| ::android::GraphicBuffer::USAGE_SW_READ_OFTEN | |
| ::android::GraphicBuffer::USAGE_SW_WRITE_OFTEN, // |
| &bufferHandle, // |
| &bufferStride, // |
| "RanchuHwc"); |
| if (status != ::android::OK) { |
| ALOGE("%s: failed to allocate composition buffer for display:%" PRIu64, |
| __FUNCTION__, displayId); |
| return HWC3::Error::NoResources; |
| } |
| |
| displayInfo.compositionResultBuffer = bufferHandle; |
| |
| auto [drmBufferCreateError, drmBuffer] = mDrmClient.create(bufferHandle); |
| if (drmBufferCreateError != HWC3::Error::None) { |
| ALOGE("%s: failed to create drm buffer for display:%" PRIu64, __FUNCTION__, |
| displayId); |
| return drmBufferCreateError; |
| } |
| displayInfo.compositionResultDrmBuffer = std::move(drmBuffer); |
| |
| if (displayId == 0) { |
| auto [flushError, flushSyncFd] = mDrmClient.flushToDisplay( |
| displayId, displayInfo.compositionResultDrmBuffer, -1); |
| if (flushError != HWC3::Error::None) { |
| ALOGW( |
| "%s: Initial display flush failed. HWComposer assuming that we are " |
| "running in QEMU without a display and disabling presenting.", |
| __FUNCTION__); |
| mPresentDisabled = true; |
| } |
| } |
| |
| std::optional<std::vector<uint8_t>> edid = mDrmClient.getEdid(displayId); |
| if (edid) { |
| display->setEdid(*edid); |
| } |
| |
| return HWC3::Error::None; |
| } |
| |
| HWC3::Error GuestFrameComposer::onDisplayDestroy(Display* display) { |
| auto displayId = display->getId(); |
| |
| auto it = mDisplayInfos.find(displayId); |
| if (it == mDisplayInfos.end()) { |
| ALOGE("%s: display:%" PRIu64 " missing display buffers?", __FUNCTION__, |
| displayId); |
| return HWC3::Error::BadDisplay; |
| } |
| |
| DisplayInfo& displayInfo = mDisplayInfos[displayId]; |
| |
| ::android::GraphicBufferAllocator::get().free( |
| displayInfo.compositionResultBuffer); |
| |
| mDisplayInfos.erase(it); |
| |
| return HWC3::Error::None; |
| } |
| |
| HWC3::Error GuestFrameComposer::onDisplayClientTargetSet(Display*) { |
| return HWC3::Error::None; |
| } |
| |
| HWC3::Error GuestFrameComposer::onActiveConfigChange(Display* /*display*/) { |
| return HWC3::Error::None; |
| }; |
| |
| HWC3::Error GuestFrameComposer::getDisplayConfigsFromDeviceConfig( |
| std::vector<GuestFrameComposer::DisplayConfig>* configs) { |
| DEBUG_LOG("%s", __FUNCTION__); |
| |
| const auto deviceConfig = cuttlefish::GetDeviceConfig(); |
| for (const auto& deviceDisplayConfig : deviceConfig.display_config()) { |
| DisplayConfig displayConfig = { |
| .width = deviceDisplayConfig.width(), |
| .height = deviceDisplayConfig.height(), |
| .dpiX = deviceDisplayConfig.dpi(), |
| .dpiY = deviceDisplayConfig.dpi(), |
| .refreshRateHz = deviceDisplayConfig.refresh_rate_hz(), |
| }; |
| |
| configs->push_back(displayConfig); |
| } |
| |
| return HWC3::Error::None; |
| } |
| |
| HWC3::Error GuestFrameComposer::getDisplayConfigsFromSystemProp( |
| std::vector<GuestFrameComposer::DisplayConfig>* configs) { |
| DEBUG_LOG("%s", __FUNCTION__); |
| |
| static constexpr const char kExternalDisplayProp[] = |
| "hwservicemanager.external.displays"; |
| |
| const auto propString = |
| ::android::base::GetProperty(kExternalDisplayProp, ""); |
| DEBUG_LOG("%s: prop value is: %s", __FUNCTION__, propString.c_str()); |
| |
| if (propString.empty()) { |
| return HWC3::Error::None; |
| } |
| |
| const std::vector<std::string> propStringParts = |
| ::android::base::Split(propString, ","); |
| if (propStringParts.size() % 5 != 0) { |
| ALOGE("%s: Invalid syntax for system prop %s which is %s", __FUNCTION__, |
| kExternalDisplayProp, propString.c_str()); |
| return HWC3::Error::BadParameter; |
| } |
| |
| std::vector<int> propIntParts; |
| for (const std::string& propStringPart : propStringParts) { |
| int propIntPart; |
| if (!::android::base::ParseInt(propStringPart, &propIntPart)) { |
| ALOGE("%s: Invalid syntax for system prop %s which is %s", __FUNCTION__, |
| kExternalDisplayProp, propString.c_str()); |
| return HWC3::Error::BadParameter; |
| } |
| propIntParts.push_back(propIntPart); |
| } |
| |
| while (!propIntParts.empty()) { |
| DisplayConfig display_config = { |
| .width = propIntParts[1], |
| .height = propIntParts[2], |
| .dpiX = propIntParts[3], |
| .dpiY = propIntParts[3], |
| .refreshRateHz = 160, |
| }; |
| |
| configs->push_back(display_config); |
| |
| propIntParts.erase(propIntParts.begin(), propIntParts.begin() + 5); |
| } |
| |
| return HWC3::Error::None; |
| } |
| |
| HWC3::Error GuestFrameComposer::validateDisplay(Display* display, |
| DisplayChanges* outChanges) { |
| const auto displayId = display->getId(); |
| DEBUG_LOG("%s display:%" PRIu64, __FUNCTION__, displayId); |
| |
| const std::vector<Layer*>& layers = display->getOrderedLayers(); |
| |
| bool fallbackToClientComposition = false; |
| for (Layer* layer : layers) { |
| const auto layerId = layer->getId(); |
| const auto layerCompositionType = layer->getCompositionType(); |
| const auto layerCompositionTypeString = toString(layerCompositionType); |
| |
| if (layerCompositionType == Composition::INVALID) { |
| ALOGE("%s display:%" PRIu64 " layer:%" PRIu64 " has Invalid composition", |
| __FUNCTION__, displayId, layerId); |
| continue; |
| } |
| |
| if (layerCompositionType == Composition::CLIENT || |
| layerCompositionType == Composition::CURSOR || |
| layerCompositionType == Composition::SIDEBAND || |
| layerCompositionType == Composition::SOLID_COLOR) { |
| DEBUG_LOG("%s: display:%" PRIu64 " layer:%" PRIu64 |
| " has composition type %s, falling back to client composition", |
| __FUNCTION__, displayId, layerId, |
| layerCompositionTypeString.c_str()); |
| fallbackToClientComposition = true; |
| break; |
| } |
| |
| if (layerCompositionType == Composition::DISPLAY_DECORATION) { |
| return HWC3::Error::Unsupported; |
| } |
| |
| if (!canComposeLayer(layer)) { |
| DEBUG_LOG( |
| "%s: display:%" PRIu64 " layer:%" PRIu64 |
| " composition not supported, falling back to client composition", |
| __FUNCTION__, displayId, layerId); |
| fallbackToClientComposition = true; |
| break; |
| } |
| } |
| |
| if (fallbackToClientComposition) { |
| for (Layer* layer : layers) { |
| const auto layerId = layer->getId(); |
| const auto layerCompositionType = layer->getCompositionType(); |
| |
| if (layerCompositionType == Composition::INVALID) { |
| continue; |
| } |
| |
| if (layerCompositionType != Composition::CLIENT) { |
| DEBUG_LOG("%s display:%" PRIu64 " layer:%" PRIu64 |
| "composition updated to Client", |
| __FUNCTION__, displayId, layerId); |
| |
| outChanges->addLayerCompositionChange(displayId, layerId, |
| Composition::CLIENT); |
| } |
| } |
| } |
| |
| // We can not draw below a Client (SurfaceFlinger) composed layer. Change all |
| // layers below a Client composed layer to also be Client composed. |
| if (layers.size() > 1) { |
| for (std::size_t layerIndex = layers.size() - 1; layerIndex > 0; |
| layerIndex--) { |
| auto layer = layers[layerIndex]; |
| auto layerCompositionType = layer->getCompositionType(); |
| |
| if (layerCompositionType == Composition::CLIENT) { |
| for (std::size_t lowerLayerIndex = 0; lowerLayerIndex < layerIndex; |
| lowerLayerIndex++) { |
| auto lowerLayer = layers[lowerLayerIndex]; |
| auto lowerLayerId = lowerLayer->getId(); |
| auto lowerLayerCompositionType = lowerLayer->getCompositionType(); |
| |
| if (lowerLayerCompositionType != Composition::CLIENT) { |
| DEBUG_LOG("%s: display:%" PRIu64 " changing layer:%" PRIu64 |
| " to Client because" |
| "hwcomposer can not draw below the Client composed " |
| "layer:%" PRIu64, |
| __FUNCTION__, displayId, lowerLayerId, layer->getId()); |
| |
| outChanges->addLayerCompositionChange(displayId, lowerLayerId, |
| Composition::CLIENT); |
| } |
| } |
| } |
| } |
| } |
| |
| return HWC3::Error::None; |
| } |
| |
| HWC3::Error GuestFrameComposer::presentDisplay( |
| Display* display, ::android::base::unique_fd* outDisplayFence, |
| std::unordered_map<int64_t, |
| ::android::base::unique_fd>* /*outLayerFences*/) { |
| const auto displayId = display->getId(); |
| DEBUG_LOG("%s display:%" PRIu64, __FUNCTION__, displayId); |
| |
| if (mPresentDisabled) { |
| return HWC3::Error::None; |
| } |
| |
| auto it = mDisplayInfos.find(displayId); |
| if (it == mDisplayInfos.end()) { |
| ALOGE("%s: display:%" PRIu64 " not found", __FUNCTION__, displayId); |
| return HWC3::Error::NoResources; |
| } |
| |
| DisplayInfo& displayInfo = it->second; |
| |
| if (displayInfo.compositionResultBuffer == nullptr) { |
| ALOGE("%s: display:%" PRIu64 " missing composition result buffer", |
| __FUNCTION__, displayId); |
| return HWC3::Error::NoResources; |
| } |
| |
| if (displayInfo.compositionResultDrmBuffer == nullptr) { |
| ALOGE("%s: display:%" PRIu64 " missing composition result drm buffer", |
| __FUNCTION__, displayId); |
| return HWC3::Error::NoResources; |
| } |
| |
| std::optional<GrallocBuffer> compositionResultBufferOpt = |
| mGralloc.Import(displayInfo.compositionResultBuffer); |
| if (!compositionResultBufferOpt) { |
| ALOGE("%s: display:%" PRIu64 " failed to import buffer", __FUNCTION__, |
| displayId); |
| return HWC3::Error::NoResources; |
| } |
| |
| std::optional<uint32_t> compositionResultBufferWidthOpt = |
| compositionResultBufferOpt->GetWidth(); |
| if (!compositionResultBufferWidthOpt) { |
| ALOGE("%s: display:%" PRIu64 " failed to query buffer width", __FUNCTION__, |
| displayId); |
| return HWC3::Error::NoResources; |
| } |
| |
| std::optional<uint32_t> compositionResultBufferHeightOpt = |
| compositionResultBufferOpt->GetHeight(); |
| if (!compositionResultBufferHeightOpt) { |
| ALOGE("%s: display:%" PRIu64 " failed to query buffer height", __FUNCTION__, |
| displayId); |
| return HWC3::Error::NoResources; |
| } |
| |
| std::optional<uint32_t> compositionResultBufferStrideOpt = |
| compositionResultBufferOpt->GetMonoPlanarStrideBytes(); |
| if (!compositionResultBufferStrideOpt) { |
| ALOGE("%s: display:%" PRIu64 " failed to query buffer stride", __FUNCTION__, |
| displayId); |
| return HWC3::Error::NoResources; |
| } |
| |
| std::optional<GrallocBufferView> compositionResultBufferViewOpt = |
| compositionResultBufferOpt->Lock(); |
| if (!compositionResultBufferViewOpt) { |
| ALOGE("%s: display:%" PRIu64 " failed to get buffer view", __FUNCTION__, |
| displayId); |
| return HWC3::Error::NoResources; |
| } |
| |
| const std::optional<void*> compositionResultBufferDataOpt = |
| compositionResultBufferViewOpt->Get(); |
| if (!compositionResultBufferDataOpt) { |
| ALOGE("%s: display:%" PRIu64 " failed to get buffer data", __FUNCTION__, |
| displayId); |
| return HWC3::Error::NoResources; |
| } |
| |
| uint32_t compositionResultBufferWidth = *compositionResultBufferWidthOpt; |
| uint32_t compositionResultBufferHeight = *compositionResultBufferHeightOpt; |
| uint32_t compositionResultBufferStride = *compositionResultBufferStrideOpt; |
| uint8_t* compositionResultBufferData = |
| reinterpret_cast<uint8_t*>(*compositionResultBufferDataOpt); |
| |
| const std::vector<Layer*>& layers = display->getOrderedLayers(); |
| |
| const bool noOpComposition = layers.empty(); |
| const bool allLayersClientComposed = |
| std::all_of(layers.begin(), // |
| layers.end(), // |
| [](const Layer* layer) { |
| return layer->getCompositionType() == Composition::CLIENT; |
| }); |
| |
| if (noOpComposition) { |
| ALOGW("%s: display:%" PRIu64 " empty composition", __FUNCTION__, displayId); |
| } else if (allLayersClientComposed) { |
| auto clientTargetBufferOpt = |
| mGralloc.Import(display->waitAndGetClientTargetBuffer()); |
| if (!clientTargetBufferOpt) { |
| ALOGE("%s: failed to import client target buffer.", __FUNCTION__); |
| return HWC3::Error::NoResources; |
| } |
| GrallocBuffer& clientTargetBuffer = *clientTargetBufferOpt; |
| |
| auto clientTargetBufferViewOpt = clientTargetBuffer.Lock(); |
| if (!clientTargetBufferViewOpt) { |
| ALOGE("%s: failed to lock client target buffer.", __FUNCTION__); |
| return HWC3::Error::NoResources; |
| } |
| GrallocBufferView& clientTargetBufferView = *clientTargetBufferViewOpt; |
| |
| auto clientTargetPlaneLayoutsOpt = clientTargetBuffer.GetPlaneLayouts(); |
| if (!clientTargetPlaneLayoutsOpt) { |
| ALOGE("Failed to get client target buffer plane layouts."); |
| return HWC3::Error::NoResources; |
| } |
| auto& clientTargetPlaneLayouts = *clientTargetPlaneLayoutsOpt; |
| |
| if (clientTargetPlaneLayouts.size() != 1) { |
| ALOGE("Unexpected number of plane layouts for client target buffer."); |
| return HWC3::Error::NoResources; |
| } |
| |
| std::size_t clientTargetPlaneSize = |
| clientTargetPlaneLayouts[0].totalSizeInBytes; |
| |
| auto clientTargetDataOpt = clientTargetBufferView.Get(); |
| if (!clientTargetDataOpt) { |
| ALOGE("%s failed to lock gralloc buffer.", __FUNCTION__); |
| return HWC3::Error::NoResources; |
| } |
| auto* clientTargetData = reinterpret_cast<uint8_t*>(*clientTargetDataOpt); |
| |
| std::memcpy(compositionResultBufferData, clientTargetData, |
| clientTargetPlaneSize); |
| } else { |
| for (Layer* layer : layers) { |
| const auto layerId = layer->getId(); |
| const auto layerCompositionType = layer->getCompositionType(); |
| if (layerCompositionType != Composition::DEVICE) { |
| continue; |
| } |
| |
| HWC3::Error error = composeLayerInto(layer, // |
| compositionResultBufferData, // |
| compositionResultBufferWidth, // |
| compositionResultBufferHeight, // |
| compositionResultBufferStride, // |
| 4); |
| if (error != HWC3::Error::None) { |
| ALOGE("%s: display:%" PRIu64 " failed to compose layer:%" PRIu64, |
| __FUNCTION__, displayId, layerId); |
| return error; |
| } |
| } |
| } |
| |
| if (display->hasColorTransform()) { |
| HWC3::Error error = |
| applyColorTransformToRGBA(display->getColorTransform(), // |
| compositionResultBufferData, // |
| compositionResultBufferWidth, // |
| compositionResultBufferHeight, // |
| compositionResultBufferStride); |
| if (error != HWC3::Error::None) { |
| ALOGE("%s: display:%" PRIu64 " failed to apply color transform", |
| __FUNCTION__, displayId); |
| return error; |
| } |
| } |
| |
| DEBUG_LOG("%s display:%" PRIu64 " flushing drm buffer", __FUNCTION__, |
| displayId); |
| |
| auto [error, fence] = mDrmClient.flushToDisplay( |
| displayId, displayInfo.compositionResultDrmBuffer, -1); |
| if (error != HWC3::Error::None) { |
| ALOGE("%s: display:%" PRIu64 " failed to flush drm buffer" PRIu64, |
| __FUNCTION__, displayId); |
| } |
| |
| *outDisplayFence = std::move(fence); |
| return error; |
| } |
| |
| bool GuestFrameComposer::canComposeLayer(Layer* layer) { |
| buffer_handle_t bufferHandle = layer->getBuffer().getBuffer(); |
| if (bufferHandle == nullptr) { |
| ALOGW("%s received a layer with a null handle", __FUNCTION__); |
| return false; |
| } |
| |
| auto bufferOpt = mGralloc.Import(bufferHandle); |
| if (!bufferOpt) { |
| ALOGE("Failed to import layer buffer."); |
| return false; |
| } |
| GrallocBuffer& buffer = *bufferOpt; |
| |
| auto bufferFormatOpt = buffer.GetDrmFormat(); |
| if (!bufferFormatOpt) { |
| ALOGE("Failed to get layer buffer format."); |
| return false; |
| } |
| uint32_t bufferFormat = *bufferFormatOpt; |
| |
| if (!IsDrmFormatSupported(bufferFormat)) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| HWC3::Error GuestFrameComposer::composeLayerInto( |
| Layer* srcLayer, // |
| std::uint8_t* dstBuffer, // |
| std::uint32_t dstBufferWidth, // |
| std::uint32_t dstBufferHeight, // |
| std::uint32_t dstBufferStrideBytes, // |
| std::uint32_t dstBufferBytesPerPixel) { |
| ATRACE_CALL(); |
| |
| libyuv::RotationMode rotation = |
| GetRotationFromTransform(srcLayer->getTransform()); |
| |
| auto srcBufferOpt = mGralloc.Import(srcLayer->waitAndGetBuffer()); |
| if (!srcBufferOpt) { |
| ALOGE("%s: failed to import layer buffer.", __FUNCTION__); |
| return HWC3::Error::NoResources; |
| } |
| GrallocBuffer& srcBuffer = *srcBufferOpt; |
| |
| auto srcBufferViewOpt = srcBuffer.Lock(); |
| if (!srcBufferViewOpt) { |
| ALOGE("%s: failed to lock import layer buffer.", __FUNCTION__); |
| return HWC3::Error::NoResources; |
| } |
| GrallocBufferView& srcBufferView = *srcBufferViewOpt; |
| |
| common::Rect srcLayerCrop = srcLayer->getSourceCropInt(); |
| common::Rect srcLayerDisplayFrame = srcLayer->getDisplayFrame(); |
| |
| auto srcLayerSpecOpt = GetBufferSpec(srcBuffer, srcBufferView, srcLayerCrop); |
| if (!srcLayerSpecOpt) { |
| return HWC3::Error::NoResources; |
| } |
| BufferSpec srcLayerSpec = *srcLayerSpecOpt; |
| |
| // TODO(jemoreira): Remove the hardcoded fomat. |
| bool needsConversion = srcLayerSpec.drmFormat != DRM_FORMAT_XBGR8888 && |
| srcLayerSpec.drmFormat != DRM_FORMAT_ABGR8888; |
| bool needsScaling = LayerNeedsScaling(*srcLayer); |
| bool needsRotation = rotation != libyuv::kRotate0; |
| bool needsTranspose = needsRotation && rotation != libyuv::kRotate180; |
| bool needsVFlip = GetVFlipFromTransform(srcLayer->getTransform()); |
| bool needsAttenuation = LayerNeedsAttenuation(*srcLayer); |
| bool needsBlending = LayerNeedsBlending(*srcLayer); |
| bool needsCopy = !(needsConversion || needsScaling || needsRotation || |
| needsVFlip || needsAttenuation || needsBlending); |
| |
| BufferSpec dstLayerSpec( |
| dstBuffer, |
| /*buffer_ycbcr=*/std::nullopt, dstBufferWidth, dstBufferHeight, |
| srcLayerDisplayFrame.left, srcLayerDisplayFrame.top, |
| srcLayerDisplayFrame.right - srcLayerDisplayFrame.left, |
| srcLayerDisplayFrame.bottom - srcLayerDisplayFrame.top, |
| DRM_FORMAT_XBGR8888, dstBufferStrideBytes, dstBufferBytesPerPixel); |
| |
| // Add the destination layer to the bottom of the buffer stack |
| std::vector<BufferSpec> dstBufferStack(1, dstLayerSpec); |
| |
| // If more than operation is to be performed, a temporary buffer is needed for |
| // each additional operation |
| |
| // N operations need N destination buffers, the destination layer (the |
| // framebuffer) is one of them, so only N-1 temporary buffers are needed. |
| // Vertical flip is not taken into account because it can be done together |
| // with any other operation. |
| int neededScratchBuffers = (needsConversion ? 1 : 0) + |
| (needsScaling ? 1 : 0) + (needsRotation ? 1 : 0) + |
| (needsAttenuation ? 1 : 0) + |
| (needsBlending ? 1 : 0) + (needsCopy ? 1 : 0) - 1; |
| |
| int mScratchBufferWidth = |
| srcLayerDisplayFrame.right - srcLayerDisplayFrame.left; |
| int mScratchBufferHeight = |
| srcLayerDisplayFrame.bottom - srcLayerDisplayFrame.top; |
| int mScratchBufferStrideBytes = |
| AlignToPower2(mScratchBufferWidth * dstBufferBytesPerPixel, 4); |
| int mScratchBufferSizeBytes = |
| mScratchBufferHeight * mScratchBufferStrideBytes; |
| |
| for (int i = 0; i < neededScratchBuffers; i++) { |
| BufferSpec mScratchBufferspec( |
| getRotatingScratchBuffer(mScratchBufferSizeBytes, i), |
| mScratchBufferWidth, mScratchBufferHeight, mScratchBufferStrideBytes); |
| dstBufferStack.push_back(mScratchBufferspec); |
| } |
| |
| // Conversion and scaling should always be the first operations, so that every |
| // other operation works on equally sized frames (guaranteed to fit in the |
| // scratch buffers). |
| |
| // TODO(jemoreira): We are converting to ARGB as the first step under the |
| // assumption that scaling ARGB is faster than scaling I420 (the most common). |
| // This should be confirmed with testing. |
| if (needsConversion) { |
| BufferSpec& dstBufferSpec = dstBufferStack.back(); |
| if (needsScaling || needsTranspose) { |
| // If a rotation or a scaling operation are needed the dimensions at the |
| // top of the buffer stack are wrong (wrong sizes for scaling, swapped |
| // width and height for 90 and 270 rotations). |
| // Make width and height match the crop sizes on the source |
| int srcWidth = srcLayerSpec.cropWidth; |
| int srcHeight = srcLayerSpec.cropHeight; |
| int dst_stride_bytes = |
| AlignToPower2(srcWidth * dstBufferBytesPerPixel, 4); |
| size_t needed_size = dst_stride_bytes * srcHeight; |
| dstBufferSpec.width = srcWidth; |
| dstBufferSpec.height = srcHeight; |
| // Adjust the stride accordingly |
| dstBufferSpec.strideBytes = dst_stride_bytes; |
| // Crop sizes also need to be adjusted |
| dstBufferSpec.cropWidth = srcWidth; |
| dstBufferSpec.cropHeight = srcHeight; |
| // cropX and y are fine at 0, format is already set to match destination |
| |
| // In case of a scale, the source frame may be bigger than the default tmp |
| // buffer size |
| dstBufferSpec.buffer = getSpecialScratchBuffer(needed_size); |
| } |
| |
| int retval = DoConversion(srcLayerSpec, dstBufferSpec, needsVFlip); |
| if (retval) { |
| ALOGE("Got error code %d from DoConversion function", retval); |
| } |
| needsVFlip = false; |
| srcLayerSpec = dstBufferSpec; |
| dstBufferStack.pop_back(); |
| } |
| |
| if (needsScaling) { |
| BufferSpec& dstBufferSpec = dstBufferStack.back(); |
| if (needsTranspose) { |
| // If a rotation is needed, the temporary buffer has the correct size but |
| // needs to be transposed and have its stride updated accordingly. The |
| // crop sizes also needs to be transposed, but not the x and y since they |
| // are both zero in a temporary buffer (and it is a temporary buffer |
| // because a rotation will be performed next). |
| std::swap(dstBufferSpec.width, dstBufferSpec.height); |
| std::swap(dstBufferSpec.cropWidth, dstBufferSpec.cropHeight); |
| // TODO (jemoreira): Aligment (To align here may cause the needed size to |
| // be bigger than the buffer, so care should be taken) |
| dstBufferSpec.strideBytes = dstBufferSpec.width * dstBufferBytesPerPixel; |
| } |
| int retval = DoScaling(srcLayerSpec, dstBufferSpec, needsVFlip); |
| needsVFlip = false; |
| if (retval) { |
| ALOGE("Got error code %d from DoScaling function", retval); |
| } |
| srcLayerSpec = dstBufferSpec; |
| dstBufferStack.pop_back(); |
| } |
| |
| if (needsRotation) { |
| int retval = |
| DoRotation(srcLayerSpec, dstBufferStack.back(), rotation, needsVFlip); |
| needsVFlip = false; |
| if (retval) { |
| ALOGE("Got error code %d from DoTransform function", retval); |
| } |
| srcLayerSpec = dstBufferStack.back(); |
| dstBufferStack.pop_back(); |
| } |
| |
| if (needsAttenuation) { |
| int retval = DoAttenuation(srcLayerSpec, dstBufferStack.back(), needsVFlip); |
| needsVFlip = false; |
| if (retval) { |
| ALOGE("Got error code %d from DoBlending function", retval); |
| } |
| srcLayerSpec = dstBufferStack.back(); |
| dstBufferStack.pop_back(); |
| } |
| |
| if (needsCopy) { |
| int retval = DoCopy(srcLayerSpec, dstBufferStack.back(), needsVFlip); |
| needsVFlip = false; |
| if (retval) { |
| ALOGE("Got error code %d from DoBlending function", retval); |
| } |
| srcLayerSpec = dstBufferStack.back(); |
| dstBufferStack.pop_back(); |
| } |
| |
| // Blending (if needed) should always be the last operation, so that it reads |
| // and writes in the destination layer and not some temporary buffer. |
| if (needsBlending) { |
| int retval = DoBlending(srcLayerSpec, dstBufferStack.back(), needsVFlip); |
| needsVFlip = false; |
| if (retval) { |
| ALOGE("Got error code %d from DoBlending function", retval); |
| } |
| // Don't need to assign destination to source in the last one |
| dstBufferStack.pop_back(); |
| } |
| |
| return HWC3::Error::None; |
| } |
| |
| namespace { |
| |
| // Returns a color matrix that can be used with libyuv by converting values |
| // in -1 to 1 into -64 to 64 and transposing. |
| std::array<std::int8_t, 16> ToLibyuvColorMatrix( |
| const std::array<float, 16>& in) { |
| std::array<std::int8_t, 16> out; |
| |
| for (int r = 0; r < 4; r++) { |
| for (int c = 0; c < 4; c++) { |
| int indexIn = (4 * r) + c; |
| int indexOut = (4 * c) + r; |
| |
| out[indexOut] = std::max( |
| -128, std::min(127, static_cast<int>(in[indexIn] * 64.0f + 0.5f))); |
| } |
| } |
| |
| return out; |
| } |
| |
| } // namespace |
| |
| HWC3::Error GuestFrameComposer::applyColorTransformToRGBA( |
| const std::array<float, 16>& transfromMatrix, // |
| std::uint8_t* buffer, // |
| std::uint32_t bufferWidth, // |
| std::uint32_t bufferHeight, // |
| std::uint32_t bufferStrideBytes) { |
| ATRACE_CALL(); |
| |
| const auto transformMatrixLibyuv = ToLibyuvColorMatrix(transfromMatrix); |
| libyuv::ARGBColorMatrix(buffer, bufferStrideBytes, // in buffer params |
| buffer, bufferStrideBytes, // out buffer params |
| transformMatrixLibyuv.data(), // |
| bufferWidth, // |
| bufferHeight); |
| |
| return HWC3::Error::None; |
| } |
| |
| uint8_t* GuestFrameComposer::getRotatingScratchBuffer(std::size_t neededSize, |
| std::uint32_t order) { |
| static constexpr const int kNumScratchBufferPieces = 2; |
| |
| std::size_t totalNeededSize = neededSize * kNumScratchBufferPieces; |
| if (mScratchBuffer.size() < totalNeededSize) { |
| mScratchBuffer.resize(totalNeededSize); |
| } |
| |
| std::size_t bufferIndex = order % kNumScratchBufferPieces; |
| std::size_t bufferOffset = bufferIndex * neededSize; |
| return &mScratchBuffer[bufferOffset]; |
| } |
| |
| uint8_t* GuestFrameComposer::getSpecialScratchBuffer(size_t neededSize) { |
| if (mSpecialScratchBuffer.size() < neededSize) { |
| mSpecialScratchBuffer.resize(neededSize); |
| } |
| |
| return &mSpecialScratchBuffer[0]; |
| } |
| |
| } // namespace aidl::android::hardware::graphics::composer3::impl |