619 lines
17 KiB
C++
619 lines
17 KiB
C++
/*
|
|
* Copyright (C) 2024 Amlogic Corporation.
|
|
*
|
|
* 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 <fcntl.h>
|
|
|
|
#include "MesonDisplay.h"
|
|
#include "V2DComposer.h"
|
|
#include "DisplayThread.h"
|
|
#include "MultiplanesComposition.h"
|
|
#include "DispAdptLog.h"
|
|
#include "MesonEventListener.h"
|
|
#include "VtAllocSolidColorBuffer.h"
|
|
|
|
MesonDisplay::MesonDisplay(uint32_t display)
|
|
{
|
|
mDisplayId = display;
|
|
mDisplayToMainPlane = false;
|
|
mEnablePrimaryPlane = false;
|
|
mDelayLatency = 0;
|
|
mPipe = std::make_shared<MesonDisplayPipe>(display);
|
|
mProcessorsManager = std::make_shared<ProcessorsManager>(
|
|
(display == 0) ? true : false);
|
|
}
|
|
|
|
MesonDisplay::~MesonDisplay()
|
|
{
|
|
if (mDisplayThread.get())
|
|
mDisplayThread.reset();
|
|
|
|
if (mVsync.get()) {
|
|
mVsync->setEnabled(false);
|
|
mVsync->setObserver(NULL);
|
|
usleep(20000);
|
|
mVsync.reset();
|
|
}
|
|
|
|
mLayers.clear();
|
|
mPlanes.clear();
|
|
mComposers.clear();
|
|
|
|
mCompositionStrategy.reset();
|
|
mPresentCompositionStg.reset();
|
|
|
|
mPipe.reset();
|
|
|
|
if (mListener.get())
|
|
mListener.reset();
|
|
}
|
|
|
|
int32_t MesonDisplay::initialize()
|
|
{
|
|
std::vector<std::shared_ptr<IComposer>> composers;
|
|
|
|
const char *env = getenv("DISPLAYADAPTER_ENABLE_PRIMARY_PLANE");
|
|
if (env && atoi(env) != 0)
|
|
mEnablePrimaryPlane = true;
|
|
|
|
VtAllocSolidColorBuffer::getInstance().allocBuffer();
|
|
|
|
getComposers(composers);
|
|
for (auto it = composers.begin(); it != composers.end(); it++) {
|
|
mComposers.emplace(2, std::move(*it));
|
|
}
|
|
|
|
if (mPipe.get()) {
|
|
if (mEnablePrimaryPlane)
|
|
mPipe->enablePrimaryPlane();
|
|
mPipe->getCrtc(mCrtc);
|
|
mPipe->getConnector(mConnector);
|
|
mPipe->getPlanes(mPlanes);
|
|
for (auto & plane : mPlanes) {
|
|
plane->setCrtcId(mCrtc->getId());
|
|
}
|
|
}
|
|
|
|
mListener = std::make_shared<MesonEventListener>(this, ADPT_LISTENER_DIR);
|
|
mListener->registerHandler(IN_CLOSE_WRITE | IN_CREATE | IN_DELETE);
|
|
mListener->addWatch(ADPT_VIDEO, [this](const std::string& path, int mask) {
|
|
char data[STR_LEN] = { 0 };
|
|
int state = 0;
|
|
if (mask & IN_CLOSE_WRITE) {
|
|
DispAdptReadFile(path.c_str(), data, true, STR_LEN);
|
|
if (sscanf(data, "%d", &state) == 1)
|
|
this->setVideoPlaneState(state);
|
|
}
|
|
});
|
|
|
|
mCompositionStrategy = std::make_shared<MultiplanesComposition>();
|
|
|
|
std::shared_ptr<HwDisplayCrtcBase> crtc =
|
|
std::static_pointer_cast<HwDisplayCrtcBase>(mCrtc);
|
|
std::shared_ptr<HwcVsyncBase> mesonVsync =
|
|
std::make_shared<HwcVsyncBase>(DISPLAY_DEFAULT);
|
|
|
|
mesonVsync->setVsyncMode(VSYNC_MODE_HW, crtc);
|
|
setVsync(mesonVsync);
|
|
|
|
if (!mDisplayThread.get()) {
|
|
mDisplayThread = std::make_shared<DisplayThread>(this);
|
|
}
|
|
mesonVsync->setEnabled(true);
|
|
|
|
initLayerIdGenerator();
|
|
return 0;
|
|
}
|
|
|
|
int32_t MesonDisplay::createV2DComposer(int idx,
|
|
std::shared_ptr<IComposer> & composer) {
|
|
int ret = 0;
|
|
int fd = -1;
|
|
char path[64] = { 0 };
|
|
|
|
snprintf(path, 64, "/dev/v2d.%u", idx);
|
|
fd = open(path, O_RDWR, 0);
|
|
if (fd >= 0) {
|
|
DISPLAY_LOGD("create V2DComposer for %s succeed", path);
|
|
composer = std::make_shared<V2DComposer>(mDisplayId, fd);
|
|
} else {
|
|
DISPLAY_LOGE("create V2DComposer for %s failed: %s", path, strerror(errno));
|
|
ret = -1;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
void MesonDisplay::getComposers(std::vector<std::shared_ptr<IComposer>> & composers)
|
|
{
|
|
int idx = 0;
|
|
int ret;
|
|
|
|
std::shared_ptr<IComposer> composer;
|
|
while (true) {
|
|
char path[64];
|
|
snprintf(path, 64, "/dev/v2d.%u", idx);
|
|
if (access(path, R_OK | W_OK) == 0)
|
|
idx ++;
|
|
else
|
|
break;
|
|
}
|
|
|
|
int id = mEnablePrimaryPlane ? 0 : 1;
|
|
composers.clear();
|
|
for (auto i = id; i < idx; i++) {
|
|
ret = createV2DComposer(i, composer);
|
|
if (ret)
|
|
return;
|
|
else
|
|
composers.push_back(std::move(composer));
|
|
}
|
|
}
|
|
|
|
/*
|
|
LayerId is 16bits.
|
|
Higher 8 Bits: now is 256 layer slot, the higher 8 bits is the slot index.
|
|
Lower 8 Bits: a sequence no, used to distinguish layers have same slot index (
|
|
which may happened a layer destroyed and at the same time a new layer created.)
|
|
*/
|
|
#define MAX_MESON_LAYERS (256)
|
|
#define LAYER_SLOT_BITS (8)
|
|
|
|
void MesonDisplay::initLayerIdGenerator()
|
|
{
|
|
mLayersBitmap = std::make_shared<BitsMap>(MAX_MESON_LAYERS);
|
|
mLayerSeq = 0;
|
|
}
|
|
|
|
uint32_t MesonDisplay::createLayerId()
|
|
{
|
|
uint32_t layerId = 0;
|
|
int idx = mLayersBitmap->getZeroBit();
|
|
mLayersBitmap->setBit(idx);
|
|
|
|
mLayerSeq++;
|
|
mLayerSeq %= MAX_MESON_LAYERS;
|
|
|
|
layerId = ((idx & (MAX_MESON_LAYERS - 1)) << LAYER_SLOT_BITS) | mLayerSeq;
|
|
return layerId;
|
|
}
|
|
|
|
void MesonDisplay::destroyLayerId(uint32_t id)
|
|
{
|
|
int slotIdx = id >> LAYER_SLOT_BITS;
|
|
mLayersBitmap->clearBit(slotIdx);
|
|
}
|
|
|
|
int MesonDisplay::createLayer(int32_t &outLayer)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
std::shared_ptr<MesonLayer> layer = std::make_shared<MesonLayer>(mDisplayId);
|
|
uint32_t idx = createLayerId();
|
|
outLayer = idx;
|
|
|
|
layer->setUniqueId(outLayer);
|
|
mLayers.emplace(outLayer, layer);
|
|
|
|
return MESON_ERROR_NONE;
|
|
}
|
|
|
|
int MesonDisplay::destroyLayer(int32_t inLayer)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
auto layerit = mLayers.find(inLayer);
|
|
if (layerit == mLayers.end())
|
|
return MESON_ERROR_BAD_LAYER;
|
|
|
|
std::shared_ptr<MesonLayer> layer = layerit->second;
|
|
mLayers.erase(inLayer);
|
|
destroyLayerId(inLayer);
|
|
|
|
if (layer.get() && !layer->isVtBuffer())
|
|
layer->releaseResource();
|
|
|
|
if (layer.get() && layer->isVtBuffer())
|
|
layer->releaseVtResource();
|
|
|
|
return MESON_ERROR_NONE;
|
|
}
|
|
|
|
int32_t MesonDisplay::setTunnelId(int32_t layer, int32_t tunnelId)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
std::shared_ptr<VtDisplayObserver> observer = std::make_shared<VtDisplayObserver>(this);
|
|
auto layerIt = mLayers.find(layer);
|
|
if (layerIt != mLayers.end())
|
|
return layerIt->second->setTunnelId(tunnelId, observer);
|
|
return -1;
|
|
}
|
|
|
|
int32_t MesonDisplay::setLayerIndex(int32_t layer, int32_t index)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
auto layerIt = mLayers.find(layer);
|
|
if (layerIt != mLayers.end())
|
|
return layerIt->second->setIndex(index);
|
|
return -1;
|
|
}
|
|
|
|
int32_t MesonDisplay::setLayerZorder(int32_t layer, int32_t zorder)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
auto layerIt = mLayers.find(layer);
|
|
if (layerIt != mLayers.end())
|
|
return layerIt->second->setZorder(zorder);
|
|
return -1;
|
|
}
|
|
|
|
int32_t MesonDisplay::setLayerSourceCrop(int32_t layerId, struct rect crop)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
drm_rect_t layer_crop;
|
|
auto layerIt = mLayers.find(layerId);
|
|
|
|
layer_crop.left = crop.x;
|
|
layer_crop.top = crop.y;
|
|
layer_crop.right = crop.x + crop.w;
|
|
layer_crop.bottom = crop.y + crop.h;
|
|
if (layerIt != mLayers.end())
|
|
return layerIt->second->setSourceCrop(layer_crop);
|
|
return -1;
|
|
}
|
|
|
|
int32_t MesonDisplay::setLayerDispFrame(int32_t layer, drm_rect_t frame)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
auto layerIt = mLayers.find(layer);
|
|
if (layerIt != mLayers.end()) {
|
|
int32_t displayW = 0;
|
|
int32_t displayH = 0;
|
|
mPipe->getSize(displayW, displayH);
|
|
if (displayW > 0 && displayH > 0) {
|
|
float scaleW = (float)displayW / DISPLAY_WIDTH_BASE;
|
|
float scaleH = (float)displayH / DISPLAY_HEIGHT_BASE;
|
|
frame.left = frame.left * scaleW;
|
|
frame.top = frame.top * scaleH;
|
|
frame.right = frame.right * scaleW;
|
|
frame.bottom = frame.bottom * scaleH;
|
|
if (frame.right > displayW) frame.right = displayW;
|
|
if (frame.bottom > displayH) frame.bottom = displayH;
|
|
}
|
|
return layerIt->second->setDisplayFrame(frame);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void MesonDisplay::setVsyncDelayLatency(int32_t latency)
|
|
{
|
|
mDelayLatency = latency;
|
|
}
|
|
|
|
int32_t MesonDisplay::getVsyncDelayLatency(int32_t layer)
|
|
{
|
|
return mDelayLatency;
|
|
}
|
|
|
|
void MesonDisplay::setVideoPlaneState(int state)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
std::string value = VIDEO_ENABLE;
|
|
bool visible = true;
|
|
int index = -1;
|
|
int change = mState != -1 ? mState ^ state : 0xf;
|
|
|
|
if (change != 0) {
|
|
if (state == 0) {
|
|
value = VIDEO_DISABLE;
|
|
}
|
|
|
|
if (mDisableValue != value)
|
|
DispAdptWriteFile(DISABLE_VIDEO, value.c_str());
|
|
|
|
mDisableValue = value;
|
|
|
|
for (auto it = mLayers.begin(); it != mLayers.end(); it++) {
|
|
index = it->second->getIndex();
|
|
|
|
if (index < 0 || index >= MAX_LAYER_NUMBER)
|
|
continue;
|
|
|
|
if ((change & (1U << BASE_SHIFT_LEFT(index))) == 0)
|
|
continue;
|
|
|
|
visible = false;
|
|
if (state & (1U << BASE_SHIFT_LEFT(index)))
|
|
visible = true;
|
|
it->second->setVisible(visible);
|
|
}
|
|
}
|
|
mState = state;
|
|
DISPLAY_LOGV("%s[%d]: state: 0x%x, change: 0x%x", __func__, __LINE__, state, change);
|
|
}
|
|
|
|
int32_t MesonDisplay::getUiSize(int32_t* width, int32_t* height)
|
|
{
|
|
if (width == NULL || height == NULL) {
|
|
DISPLAY_LOGE(" Invalid pointer! width or height is NULL");
|
|
return -1;
|
|
}
|
|
*width = DISPLAY_WIDTH_BASE;
|
|
*height = DISPLAY_HEIGHT_BASE;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t MesonDisplay::setVideoDisplaytoMainplane()
|
|
{
|
|
char path_select[MESON_MAX_STR_LEN] = { 0 };
|
|
int vd1, vd2, vd3;
|
|
int ret;
|
|
|
|
if (mEnablePrimaryPlane)
|
|
return 0;
|
|
|
|
ret = DispAdptReadFile(VIDEO_PATH_SELECT, path_select, true, MESON_MAX_STR_LEN);
|
|
|
|
if (ret) {
|
|
DISPLAY_LOGE("read file(%s) fail", VIDEO_PATH_SELECT);
|
|
return ret;
|
|
}
|
|
|
|
if (sscanf(path_select, "vd1: %d vd2: %d, vd3: %d", &vd1, &vd2, &vd3) == 3) {
|
|
memset(path_select, 0, sizeof(path_select));
|
|
sprintf(path_select, "%d %d %d", vd2, vd1, vd3);
|
|
DispAdptWriteFile(VIDEO_PATH_SELECT, path_select);
|
|
DispAdptWriteFile(DISABLE_VIDEO, "0");
|
|
DispAdptWriteFile(VIDEO_TEST_PATTERN, "0 1 0xff");
|
|
DispAdptWriteFile(VIDEO_TEST_PATTERN, "0 0 0xff");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int32_t MesonDisplay::setVsync(std::shared_ptr<HwcVsyncBase> vsync)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
int vsyncType = vsync->getVsyncType();
|
|
switch (vsyncType) {
|
|
case DISPLAY_DEFAULT:
|
|
if (mVsync != vsync) {
|
|
if (mVsync.get()) {
|
|
mVsync->setEnabled(false);
|
|
mVsync->setObserver(NULL);
|
|
} else {
|
|
mVsync = vsync;
|
|
mVsync->setObserver(this);
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
DISPLAY_LOGE("Get invalid vsync");
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void MesonDisplay::onVsync(int64_t timestamp, uint32_t vsyncPeriodNanos, int vsyncType)
|
|
{
|
|
switch (vsyncType) {
|
|
case DISPLAY_DEFAULT:
|
|
mVsyncTimestamp = timestamp;
|
|
if (mDisplayThread.get()) {
|
|
mDisplayThread->onVsync(timestamp, vsyncPeriodNanos);
|
|
} else {
|
|
DISPLAY_LOGE("MesonDisplay (%p) mDisplayThread is NULL", this);
|
|
}
|
|
break;
|
|
default:
|
|
DISPLAY_LOGE("onVsync get invalid vsync");
|
|
break;
|
|
}
|
|
}
|
|
|
|
void MesonDisplay::setLayersPresentTime()
|
|
{
|
|
std::lock_guard<std::mutex> vtLock(mMutex);
|
|
|
|
nsecs_t period = 1e9 / 60;
|
|
|
|
nsecs_t expectPresentedTime = mVsyncTimestamp + period;
|
|
|
|
for (auto it = mLayers.begin(); it != mLayers.end(); it++) {
|
|
// expectPresent time is current vsync timestamp + 1 vsyncPeriod
|
|
it->second->setPresentTime(expectPresentedTime);
|
|
}
|
|
}
|
|
|
|
int MesonDisplay::collectLayersForPresent() {
|
|
/*
|
|
* 1) add reference to Layers to keep it alive during display.
|
|
* 2) for special layer, update its composition type.
|
|
* 3) sort Layers by zorder for composition.
|
|
*/
|
|
mPresentLayers.reserve(10);
|
|
|
|
for (auto it = mLayers.begin(); it != mLayers.end(); it++) {
|
|
mPresentLayers.push_back(it->second);
|
|
mFbs.insert(make_pair(it->second->getCurZorder(), it->second));
|
|
}
|
|
return MESON_ERROR_NONE;
|
|
}
|
|
|
|
int MesonDisplay::collectPlanesForPresent() {
|
|
mPresentPlanes = mPlanes;
|
|
return MESON_ERROR_NONE;
|
|
}
|
|
|
|
int MesonDisplay::collectComposersForPresent() {
|
|
for (auto it = mComposers.begin(); it != mComposers.end(); it++) {
|
|
mPresentComposers.push_back(it->second);
|
|
}
|
|
|
|
return MESON_ERROR_NONE;
|
|
}
|
|
|
|
int MesonDisplay::collectCompositionStgForPresent() {
|
|
if (!mCompositionStrategy.get())
|
|
return MESON_ERROR_BAD_PARAMETER;
|
|
mPresentCompositionStg = mCompositionStrategy;
|
|
return MESON_ERROR_NONE;
|
|
}
|
|
|
|
int MesonDisplay::validateDisplay(uint32_t* outNumTypes,
|
|
uint32_t* outNumRequests)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
bool updateZ = true;
|
|
|
|
mPresentLayers.clear();
|
|
mPresentComposers.clear();
|
|
mPresentPlanes.clear();
|
|
mPresentCompositionStg.reset();
|
|
mFbs.clear();
|
|
|
|
int ret = collectLayersForPresent();
|
|
if (ret != MESON_ERROR_NONE) {
|
|
return ret;
|
|
}
|
|
ret = collectComposersForPresent();
|
|
if (ret != MESON_ERROR_NONE) {
|
|
return ret;
|
|
}
|
|
ret = collectPlanesForPresent();
|
|
if (ret != MESON_ERROR_NONE) {
|
|
return ret;
|
|
}
|
|
ret = collectCompositionStgForPresent();
|
|
if (ret != MESON_ERROR_NONE) {
|
|
return ret;
|
|
}
|
|
|
|
if (mFbs.size() != mLayers.size())
|
|
updateZ = false;
|
|
|
|
/*setup composition strategy.*/
|
|
std::vector<std::shared_ptr<DrmFramebufferBase>> fbs;
|
|
auto fbIt = mPresentLayers.begin();
|
|
for (; fbIt != mPresentLayers.end(); fbIt++) {
|
|
if (updateZ)
|
|
(*fbIt)->updateZorder();
|
|
fbs.push_back(std::static_pointer_cast<DrmFramebufferBase>(*fbIt));
|
|
}
|
|
|
|
std::vector<std::shared_ptr<HwDisplayPlaneBase>> planes;
|
|
auto planeIt = mPresentPlanes.begin();
|
|
for (; planeIt != mPresentPlanes.end(); planeIt++) {
|
|
if ((*planeIt)->getPrimary() || !mDisplayToMainPlane)
|
|
planes.push_back(std::static_pointer_cast<HwDisplayPlaneBase>(*planeIt));
|
|
}
|
|
|
|
std::shared_ptr<HwDisplayCrtcBase> crtc =
|
|
std::static_pointer_cast<HwDisplayCrtcBase>(mCrtc);
|
|
mPresentCompositionStg->setup(fbs, mPresentComposers,
|
|
planes, crtc, 0, 0, { 0 });
|
|
|
|
if (mPresentCompositionStg->decideComposition() < 0)
|
|
return MESON_ERROR_NO_RESOURCES;
|
|
|
|
if (mProcessorsManager.get())
|
|
mProcessorsManager->setup(mPresentLayers, 0);
|
|
|
|
return ret;
|
|
}
|
|
|
|
int MesonDisplay::presentDisplay(int32_t* outPresentFence)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
int ret;
|
|
bool mainThread = !hasVtBuffer();
|
|
|
|
if (mPresentCompositionStg.get() == NULL)
|
|
return MESON_ERROR_BAD_PARAMETER;
|
|
|
|
if (mProcessorsManager.get())
|
|
mProcessorsManager->runVideoProcessors(mainThread);
|
|
|
|
ret = mPresentCompositionStg->commit();
|
|
|
|
if (mProcessorsManager.get())
|
|
mProcessorsManager->setVideoProcessorReleaseFence(ret != 0, mainThread);
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool MesonDisplay::handleDisplayConnection()
|
|
{
|
|
std::lock_guard<std::mutex> vtLock(mMutex);
|
|
std::shared_ptr<MesonLayer> layer;
|
|
|
|
for (auto it = mLayers.begin(); it != mLayers.end(); it++) {
|
|
layer = it->second;
|
|
if (!layer->isVtBuffer())
|
|
layer->updateBuffer();
|
|
else
|
|
layer->updateVtBuffer();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void MesonDisplay::releaseLayers()
|
|
{
|
|
std::lock_guard<std::mutex> vtLock(mMutex);
|
|
int ret;
|
|
std::shared_ptr<MesonLayer> layer;
|
|
|
|
for (auto it = mLayers.begin(); it != mLayers.end(); it++) {
|
|
layer = it->second;
|
|
if (!layer->isVtBuffer()) {
|
|
ret = layer->releaseBuffer();
|
|
} else {
|
|
ret = layer->releaseVtBuffer();
|
|
}
|
|
if (ret != 0 && ret != -EAGAIN) {
|
|
DISPLAY_LOGE("%s, release layer id=%" PRIu64 " failed, ret=%d",
|
|
__func__, layer->getUniqueId(), ret);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* whether the videotunnel layer has new game buffer
|
|
*/
|
|
bool MesonDisplay::newGameBuffer() {
|
|
std::lock_guard<std::mutex> vtLock(mMutex);
|
|
for (auto it = mLayers.begin(); it != mLayers.end(); it++) {
|
|
auto layer = it->second;
|
|
if (layer && layer->newGameBuffer())
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void MesonDisplay::onFrameAvailable() {
|
|
if (mDisplayThread.get())
|
|
mDisplayThread->onFrameAvailableForGameMode();
|
|
}
|
|
|
|
bool MesonDisplay::hasVtBuffer() {
|
|
for (auto it = mLayers.begin(); it != mLayers.end(); it++) {
|
|
auto layer = it->second;
|
|
if (layer && layer->isVtBuffer())
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|