Codec2: Add classes and interfaces for blocking allocator
Add classes for blocking allocator.
- C2Fence implementation for blocking allocator
- Synchronization between processes implementation
Bug: 157111613
Change-Id: Id9f122261c04e57205d78296112980f5b34343f5
diff --git a/media/codec2/vndk/Android.bp b/media/codec2/vndk/Android.bp
index 0401c1d..be81c84 100644
--- a/media/codec2/vndk/Android.bp
+++ b/media/codec2/vndk/Android.bp
@@ -36,9 +36,11 @@
"C2Buffer.cpp",
"C2Config.cpp",
"C2DmaBufAllocator.cpp",
+ "C2Fence.cpp",
"C2PlatformStorePluginLoader.cpp",
"C2Store.cpp",
"platform/C2BqBuffer.cpp",
+ "platform/C2SurfaceSyncObj.cpp",
"types.cpp",
"util/C2Debug.cpp",
"util/C2InterfaceHelper.cpp",
diff --git a/media/codec2/vndk/C2Fence.cpp b/media/codec2/vndk/C2Fence.cpp
new file mode 100644
index 0000000..9c5183e
--- /dev/null
+++ b/media/codec2/vndk/C2Fence.cpp
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+//#define LOG_NDEBUG 0
+#define LOG_TAG "C2FenceFactory"
+#include <utils/Log.h>
+
+#include <C2FenceFactory.h>
+#include <C2SurfaceSyncObj.h>
+
+class C2Fence::Impl {
+public:
+ virtual c2_status_t wait(c2_nsecs_t timeoutNs) = 0;
+
+ virtual bool valid() const = 0;
+
+ virtual bool ready() const = 0;
+
+ virtual int fd() const = 0;
+
+ virtual bool isHW() const = 0;
+
+ virtual ~Impl() = default;
+
+ Impl() = default;
+};
+
+c2_status_t C2Fence::wait(c2_nsecs_t timeoutNs) {
+ if (mImpl) {
+ return mImpl->wait(timeoutNs);
+ }
+ // null fence is always signalled.
+ return C2_OK;
+}
+
+bool C2Fence::valid() const {
+ if (mImpl) {
+ return mImpl->valid();
+ }
+ // null fence is always valid.
+ return true;
+}
+
+bool C2Fence::ready() const {
+ if (mImpl) {
+ return mImpl->ready();
+ }
+ // null fence is always signalled.
+ return true;
+}
+
+int C2Fence::fd() const {
+ if (mImpl) {
+ return mImpl->fd();
+ }
+ // null fence does not have fd.
+ return -1;
+}
+
+bool C2Fence::isHW() const {
+ if (mImpl) {
+ return mImpl->isHW();
+ }
+ return false;
+}
+
+/**
+ * Fence implementation for C2BufferQueueBlockPool based block allocation.
+ * The implementation supports all C2Fence interface except fd().
+ */
+class _C2FenceFactory::SurfaceFenceImpl: public C2Fence::Impl {
+public:
+ virtual c2_status_t wait(c2_nsecs_t timeoutNs) {
+ if (mPtr) {
+ return mPtr->waitForChange(mWaitId, timeoutNs);
+ }
+ return C2_OK;
+ }
+
+ virtual bool valid() const {
+ return mPtr;
+ }
+
+ virtual bool ready() const {
+ uint32_t status;
+ if (mPtr) {
+ mPtr->lock();
+ status = mPtr->getWaitIdLocked();
+ mPtr->unlock();
+
+ return status != mWaitId;
+ }
+ return true;
+ }
+
+ virtual int fd() const {
+ // does not support fd, since this is shared mem and futex based
+ return -1;
+ }
+
+ virtual bool isHW() const {
+ return false;
+ }
+
+ virtual ~SurfaceFenceImpl() {};
+
+ SurfaceFenceImpl(std::shared_ptr<C2SurfaceSyncMemory> syncMem, uint32_t waitId) :
+ mSyncMem(syncMem),
+ mPtr(syncMem ? syncMem->mem() : nullptr),
+ mWaitId(syncMem ? waitId : 0) {}
+private:
+ const std::shared_ptr<const C2SurfaceSyncMemory> mSyncMem; // This is for life-cycle guarantee
+ C2SyncVariables *const mPtr;
+ const uint32_t mWaitId;
+};
+
+C2Fence::C2Fence(std::shared_ptr<Impl> impl) : mImpl(impl) {}
+
+C2Fence _C2FenceFactory::CreateSurfaceFence(
+ std::shared_ptr<C2SurfaceSyncMemory> syncMem,
+ uint32_t waitId) {
+ if (syncMem) {
+ C2Fence::Impl *p
+ = new _C2FenceFactory::SurfaceFenceImpl(syncMem, waitId);
+ if (p->valid()) {
+ return C2Fence(std::shared_ptr<C2Fence::Impl>(p));
+ } else {
+ delete p;
+ }
+ }
+ return C2Fence();
+}
diff --git a/media/codec2/vndk/include/C2FenceFactory.h b/media/codec2/vndk/include/C2FenceFactory.h
new file mode 100644
index 0000000..d4bed26
--- /dev/null
+++ b/media/codec2/vndk/include/C2FenceFactory.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+#ifndef STAGEFRIGHT_CODEC2_FENCE_FACTORY_H_
+#define STAGEFRIGHT_CODEC2_FENCE_FACTORY_H_
+
+
+#include <C2Buffer.h>
+
+class C2SurfaceSyncMemory;
+
+/**
+ * C2Fence implementation factory
+ */
+struct _C2FenceFactory {
+
+ class SurfaceFenceImpl;
+
+ /*
+ * Create C2Fence for BufferQueueBased blockpool.
+ *
+ * \param syncMem Shared memory object for synchronization between processes.
+ * \param waitId wait id for tracking status change for C2Fence.
+ */
+ static C2Fence CreateSurfaceFence(
+ std::shared_ptr<C2SurfaceSyncMemory> syncMem,
+ uint32_t waitId);
+};
+
+
+#endif // STAGEFRIGHT_CODEC2_FENCE_FACTORY_H_
diff --git a/media/codec2/vndk/include/C2SurfaceSyncObj.h b/media/codec2/vndk/include/C2SurfaceSyncObj.h
new file mode 100644
index 0000000..16e9a9d
--- /dev/null
+++ b/media/codec2/vndk/include/C2SurfaceSyncObj.h
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+#ifndef STAGEFRIGHT_CODEC2_SURFACE_SYNC_OBJ_H_
+#define STAGEFRIGHT_CODEC2_SURFACE_SYNC_OBJ_H_
+
+#include <cutils/native_handle.h>
+#include <memory>
+#include <atomic>
+
+#include <C2Buffer.h>
+
+/**
+ * Futex based lock / wait implementation for sharing output buffer allocation
+ * information between Framework and HAL.
+ */
+struct C2SyncVariables {
+ enum SyncStatus : uint32_t {
+ STATUS_INIT = 0, // When surface configuration starts.
+ STATUS_ACTIVE = 1, // When surface configuration finishs.
+ // STATUS_INIT -> STATUS_ACTIVE
+ STATUS_SWITCHING = 2, // When the surface is replaced by a new surface
+ // during surface configuration.
+ // STATUS_ACTIVE -> STATUS_SWITCHING
+ };
+
+ /**
+ * Lock the memory region
+ */
+ int lock();
+
+ /**
+ * Unlock the memory region
+ */
+ int unlock();
+
+ /**
+ * Set initial dequeued buffer count.
+ *
+ * \param maxDequeueCount Initial value of # of max dequeued buffer count
+ * \param curDequeueCount Initial value of # of current dequeued buffer count
+ */
+ void setInitialDequeueCount(int32_t maxDequeueCount, int32_t curDequeueCount);
+
+ /**
+ * Get a waitId which will be used to implement fence.
+ */
+ uint32_t getWaitIdLocked();
+
+ /**
+ * Return whether the upcoming dequeue operation is not blocked.
+ * if it's blocked and waitId is non-null, waitId is returned to be used for waiting.
+ *
+ * \retval false dequeue operation is blocked now.
+ * \retval true dequeue operation is possible.
+ */
+ bool isDequeueableLocked(uint32_t *waitId = nullptr);
+
+ /**
+ * Notify a buffer is queued. Return whether the upcoming dequeue operation
+ * is not blocked. if it's blocked and waitId is non-null, waitId is returned
+ * to be used for waiting.
+ *
+ * \retval false dequeue operation is blocked now.
+ * \retval true dequeue operation is possible.
+ */
+ bool notifyQueuedLocked(uint32_t *waitId = nullptr);
+
+ /**
+ * Notify a buffer is dequeued.
+ */
+ void notifyDequeuedLocked();
+
+ /**
+ * Set sync status.
+ */
+ void setSyncStatusLocked(SyncStatus status);
+
+ /**
+ * Get sync status.
+ */
+ C2SyncVariables::SyncStatus getSyncStatusLocked();
+
+ /**
+ * Update current max dequeue count.
+ */
+ void updateMaxDequeueCountLocked(int32_t maxDequeueCount);
+
+ /**
+ * Wait until status is no longer equal to waitId, or until timeout.
+ *
+ * \param waitId internal status for waiting until it is changed.
+ * \param timeousNs nano seconds to timeout.
+ *
+ * \retval C2_TIMEDOUT change does not happen during waiting.
+ * \retval C2_BAD_VALUE invalid event waiting.
+ * \retval C2_OK change was signalled.
+ */
+ c2_status_t waitForChange(uint32_t waitId, c2_nsecs_t timeoutNs);
+
+ C2SyncVariables() {}
+
+private:
+ /**
+ * signal one waiter to wake up.
+ */
+ int signal();
+
+ /**
+ * signal all waiter to wake up.
+ */
+ int broadcast();
+
+ /**
+ * wait for signal or broadcast.
+ */
+ int wait();
+
+ std::atomic<uint32_t> mLock;
+ std::atomic<uint32_t> mCond;
+ int32_t mMaxDequeueCount;
+ int32_t mCurDequeueCount;
+ SyncStatus mStatus;
+};
+
+/**
+ * Shared memory in order to synchronize information for Surface(IGBP)
+ * based output buffer allocation.
+ */
+class C2SurfaceSyncMemory {
+public:
+ /**
+ * Shared memory handle in order to synchronize information for
+ * Surface based output buffer allocation.
+ */
+ struct HandleSyncMem : public native_handle_t {
+ HandleSyncMem(int fd, size_t size) :
+ native_handle_t(cHeader),
+ mFds{fd},
+ mInts{int(size & 0xFFFFFFFF),
+ int((uint64_t(size) >> 32) & 0xFFFFFFFF), kMagic} {}
+
+ /** Returns a file descriptor of the shared memory
+ * \return a file descriptor representing the shared memory
+ */
+ int memFd() const {return mFds.mMem;}
+
+ /** Returns the size of the shared memory */
+ size_t size() const {
+ return size_t(unsigned(mInts.mSizeLo))
+ | size_t(uint64_t(unsigned(mInts.mSizeHi)) << 32);
+ }
+
+ /** Check whether the native handle is in the form of HandleSyncMem
+ *
+ * \return whether the native handle is compatible
+ */
+ static bool isValid(const native_handle_t * const o);
+
+ protected:
+ struct {
+ int mMem;
+ } mFds;
+ struct {
+ int mSizeLo;
+ int mSizeHi;
+ int mMagic;
+ } mInts;
+ private:
+ enum {
+ kMagic = 'ssm\x00',
+ numFds = sizeof(mFds) / sizeof(int),
+ numInts = sizeof(mInts) / sizeof(int),
+ version = sizeof(native_handle_t)
+ };
+ const static native_handle_t cHeader;
+ };
+
+ /**
+ * Imports a shared memory object from a native handle(The shared memory is already existing).
+ * This is usually used after native_handle_t is passed via RPC.
+ *
+ * \param handle handle representing shared memory for output buffer allocation.
+ */
+ static std::shared_ptr<C2SurfaceSyncMemory> Import(native_handle_t *handle);
+
+ /**
+ * Creats a shared memory object for synchronization of output buffer allocation.
+ * Shared memory creation should be done explicitly.
+ *
+ * \param fd file descriptor to shared memory
+ * \param size size of the shared memory
+ */
+ static std::shared_ptr<C2SurfaceSyncMemory> Create(int fd, size_t size);
+
+ /**
+ * Returns a handle representing the shread memory for synchronization of
+ * output buffer allocation.
+ */
+ native_handle_t *handle();
+
+ /**
+ * Returns synchronization object which will provide synchronization primitives.
+ *
+ * \return a ptr to synchronization primitive class
+ */
+ C2SyncVariables *mem();
+
+ ~C2SurfaceSyncMemory();
+
+private:
+ bool mInit;
+ HandleSyncMem *mHandle;
+ C2SyncVariables *mMem;
+
+ C2SurfaceSyncMemory();
+};
+
+#endif // STAGEFRIGHT_CODEC2_SURFACE_SYNC_OBJ_H_
diff --git a/media/codec2/vndk/platform/C2SurfaceSyncObj.cpp b/media/codec2/vndk/platform/C2SurfaceSyncObj.cpp
new file mode 100644
index 0000000..587992e
--- /dev/null
+++ b/media/codec2/vndk/platform/C2SurfaceSyncObj.cpp
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+//#define LOG_NDEBUG 0
+#define LOG_TAG "C2SurfaceSyncObj"
+#include <limits.h>
+#include <linux/futex.h>
+#include <sys/mman.h>
+#include <sys/syscall.h>
+#include <sys/time.h>
+#include <utils/Log.h>
+
+#include <chrono>
+#include <C2SurfaceSyncObj.h>
+
+const native_handle_t C2SurfaceSyncMemory::HandleSyncMem::cHeader = {
+ C2SurfaceSyncMemory::HandleSyncMem::version,
+ C2SurfaceSyncMemory::HandleSyncMem::numFds,
+ C2SurfaceSyncMemory::HandleSyncMem::numInts,
+ {}
+};
+
+bool C2SurfaceSyncMemory::HandleSyncMem::isValid(const native_handle_t * const o) {
+ if (!o || memcmp(o, &cHeader, sizeof(cHeader))) {
+ return false;
+ }
+
+ const HandleSyncMem *other = static_cast<const HandleSyncMem*>(o);
+ return other->mInts.mMagic == kMagic;
+}
+
+C2SurfaceSyncMemory::C2SurfaceSyncMemory()
+ : mInit(false), mHandle(nullptr), mMem(nullptr) {}
+
+C2SurfaceSyncMemory::~C2SurfaceSyncMemory() {
+ if (mInit) {
+ if (mMem) {
+ munmap(static_cast<void *>(mMem), mHandle->size());
+ }
+ if (mHandle) {
+ native_handle_close(mHandle);
+ native_handle_delete(mHandle);
+ }
+ }
+}
+
+std::shared_ptr<C2SurfaceSyncMemory> C2SurfaceSyncMemory::Import(
+ native_handle_t *handle) {
+ if (!HandleSyncMem::isValid(handle)) {
+ return nullptr;
+ }
+
+ HandleSyncMem *o = static_cast<HandleSyncMem*>(handle);
+ void *ptr = mmap(NULL, o->size(), PROT_READ | PROT_WRITE, MAP_SHARED, o->memFd(), 0);
+
+ if (ptr == MAP_FAILED) {
+ native_handle_close(handle);
+ native_handle_delete(handle);
+ return nullptr;
+ }
+
+ std::shared_ptr<C2SurfaceSyncMemory> syncMem(new C2SurfaceSyncMemory);
+ syncMem->mInit = true;
+ syncMem->mHandle = o;
+ syncMem->mMem = static_cast<C2SyncVariables*>(ptr);
+ return syncMem;
+}
+
+std::shared_ptr<C2SurfaceSyncMemory> C2SurfaceSyncMemory::Create(int fd, size_t size) {
+ if (fd < 0 || size == 0) {
+ return nullptr;
+ }
+ HandleSyncMem *handle = new HandleSyncMem(fd, size);
+
+ void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ if (ptr == MAP_FAILED) {
+ native_handle_close(handle);
+ native_handle_delete(handle);
+ return nullptr;
+ }
+ memset(ptr, 0, size);
+
+ std::shared_ptr<C2SurfaceSyncMemory> syncMem(new C2SurfaceSyncMemory);
+ syncMem->mInit = true;
+ syncMem->mHandle = handle;
+ syncMem->mMem = static_cast<C2SyncVariables*>(ptr);
+ return syncMem;
+}
+
+native_handle_t *C2SurfaceSyncMemory::handle() {
+ return !mInit ? nullptr : mHandle;
+}
+
+C2SyncVariables *C2SurfaceSyncMemory::mem() {
+ return !mInit ? nullptr : mMem;
+}
+
+namespace {
+ constexpr int kSpinNumForLock = 100;
+ constexpr int kSpinNumForUnlock = 200;
+
+ enum : uint32_t {
+ FUTEX_UNLOCKED = 0,
+ FUTEX_LOCKED_UNCONTENDED = 1, // user-space locking
+ FUTEX_LOCKED_CONTENDED = 2, // futex locking
+ };
+}
+
+int C2SyncVariables::lock() {
+ uint32_t old;
+ for (int i = 0; i < kSpinNumForLock; i++) {
+ old = 0;
+ if (mLock.compare_exchange_strong(old, FUTEX_LOCKED_UNCONTENDED)) {
+ return 0;
+ }
+ sched_yield();
+ }
+
+ if (old == FUTEX_LOCKED_UNCONTENDED)
+ old = mLock.exchange(FUTEX_LOCKED_CONTENDED);
+
+ while (old) {
+ (void) syscall(__NR_futex, &mLock, FUTEX_WAIT, FUTEX_LOCKED_CONTENDED, NULL, NULL, 0);
+ old = mLock.exchange(FUTEX_LOCKED_CONTENDED);
+ }
+ return 0;
+}
+
+int C2SyncVariables::unlock() {
+ if (mLock.exchange(FUTEX_UNLOCKED) == FUTEX_LOCKED_UNCONTENDED) return 0;
+
+ for (int i = 0; i < kSpinNumForUnlock; i++) {
+ if (mLock.load()) {
+ uint32_t old = FUTEX_LOCKED_UNCONTENDED;
+ mLock.compare_exchange_strong(old, FUTEX_LOCKED_CONTENDED);
+ if (old) {
+ return 0;
+ }
+ }
+ sched_yield();
+ }
+
+ (void) syscall(__NR_futex, &mLock, FUTEX_WAKE, 1, NULL, NULL, 0);
+ return 0;
+}
+
+void C2SyncVariables::setInitialDequeueCount(
+ int32_t maxDequeueCount, int32_t curDequeueCount) {
+ lock();
+ mMaxDequeueCount = maxDequeueCount;
+ mCurDequeueCount = curDequeueCount;
+ unlock();
+}
+
+uint32_t C2SyncVariables::getWaitIdLocked() {
+ return mCond.load();
+}
+
+bool C2SyncVariables::isDequeueableLocked(uint32_t *waitId) {
+ if (mMaxDequeueCount <= mCurDequeueCount) {
+ if (waitId) {
+ *waitId = getWaitIdLocked();
+ }
+ return false;
+ }
+ return true;
+}
+
+bool C2SyncVariables::notifyQueuedLocked(uint32_t *waitId) {
+ // Note. thundering herds may occur. Edge trigged signalling.
+ // But one waiter will guarantee to dequeue. others may wait again.
+ // Minimize futex syscall(trap) for the main use case(one waiter case).
+ if (mMaxDequeueCount == mCurDequeueCount--) {
+ broadcast();
+ return true;
+ }
+
+ if (mCurDequeueCount >= mMaxDequeueCount) {
+ if (waitId) {
+ *waitId = getWaitIdLocked();
+ }
+ ALOGV("dequeue blocked %d/%d", mCurDequeueCount, mMaxDequeueCount);
+ return false;
+ }
+ return true;
+}
+
+void C2SyncVariables::notifyDequeuedLocked() {
+ mCurDequeueCount++;
+ ALOGV("dequeue successful %d/%d", mCurDequeueCount, mMaxDequeueCount);
+}
+
+void C2SyncVariables::setSyncStatusLocked(SyncStatus status) {
+ mStatus = status;
+ if (mStatus == STATUS_ACTIVE) {
+ broadcast();
+ }
+}
+
+C2SyncVariables::SyncStatus C2SyncVariables::getSyncStatusLocked() {
+ return mStatus;
+}
+
+void C2SyncVariables::updateMaxDequeueCountLocked(int32_t maxDequeueCount) {
+ mMaxDequeueCount = maxDequeueCount;
+ if (mStatus == STATUS_ACTIVE) {
+ broadcast();
+ }
+}
+
+c2_status_t C2SyncVariables::waitForChange(uint32_t waitId, c2_nsecs_t timeoutNs) {
+ if (timeoutNs < 0) {
+ timeoutNs = 0;
+ }
+ struct timespec tv;
+ tv.tv_sec = timeoutNs / 1000000000;
+ tv.tv_nsec = timeoutNs % 1000000000;
+
+ int ret = syscall(__NR_futex, &mCond, FUTEX_WAIT, waitId, &tv, NULL, 0);
+ if (ret == 0 || ret == EAGAIN) {
+ return C2_OK;
+ }
+ if (ret == EINTR || ret == ETIMEDOUT) {
+ return C2_TIMED_OUT;
+ }
+ return C2_BAD_VALUE;
+}
+
+int C2SyncVariables::signal() {
+ mCond++;
+
+ (void) syscall(__NR_futex, &mCond, FUTEX_WAKE, 1, NULL, NULL, 0);
+ return 0;
+}
+
+int C2SyncVariables::broadcast() {
+ mCond++;
+
+ (void) syscall(__NR_futex, &mCond, FUTEX_REQUEUE, 1, (void *)INT_MAX, &mLock, 0);
+ return 0;
+}
+
+int C2SyncVariables::wait() {
+ uint32_t old = mCond.load();
+ unlock();
+
+ (void) syscall(__NR_futex, &mCond, FUTEX_WAIT, old, NULL, NULL, 0);
+ while (mLock.exchange(FUTEX_LOCKED_CONTENDED)) {
+ (void) syscall(__NR_futex, &mLock, FUTEX_WAIT, FUTEX_LOCKED_CONTENDED, NULL, NULL, 0);
+ }
+ return 0;
+}