зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1357133 - Recover from incorrectly guessing the CDM's shmem sizes. r=gerald
We are pre-allocating shmems in the content process for use by the CDM in the GMP process. We guess the size of shmem required. However if we guess wrong, currently we always end up taking the non-shmem path for video frames to return to the content process, which results in us sending another shmem (of the wrong size) to the CDM, and this continues until we hit the limit on the number of shmems that we tolerate the CDM asking for. So in this patch, I change our behaviour to detect when we're allocating shmems that are too small, whereupon we purge the existing shmems and switch to allocating them at the size being requested by the CDM. This means we recover from incorrectly guessing the size of shmems required by the CDM. The overhead of an incorrect guess should be one video frame transferred via the nsTArray path. MozReview-Commit-ID: 8o1s7FI2UBd --HG-- extra : rebase_source : 0612d199686278612e8c58dc97e96a9304ea3ee9
This commit is contained in:
Родитель
c9e89efdd7
Коммит
5fe1464d84
|
@ -371,6 +371,22 @@ ChromiumCDMChild::IsOnMessageLoopThread()
|
|||
return mPlugin && mPlugin->GMPMessageLoop() == MessageLoop::current();
|
||||
}
|
||||
|
||||
void
|
||||
ChromiumCDMChild::PurgeShmems()
|
||||
{
|
||||
for (ipc::Shmem& shmem : mBuffers) {
|
||||
DeallocShmem(shmem);
|
||||
}
|
||||
mBuffers.Clear();
|
||||
}
|
||||
|
||||
ipc::IPCResult
|
||||
ChromiumCDMChild::RecvPurgeShmems()
|
||||
{
|
||||
PurgeShmems();
|
||||
return IPC_OK();
|
||||
}
|
||||
|
||||
mozilla::ipc::IPCResult
|
||||
ChromiumCDMChild::RecvInit(const bool& aAllowDistinctiveIdentifier,
|
||||
const bool& aAllowPersistentState)
|
||||
|
@ -639,10 +655,7 @@ ChromiumCDMChild::RecvDeinitializeVideoDecoder()
|
|||
mDecoderInitialized = false;
|
||||
mCDM->DeinitializeDecoder(cdm::kStreamTypeVideo);
|
||||
}
|
||||
for (ipc::Shmem& shmem : mBuffers) {
|
||||
DeallocShmem(shmem);
|
||||
}
|
||||
mBuffers.Clear();
|
||||
PurgeShmems();
|
||||
return IPC_OK();
|
||||
}
|
||||
|
||||
|
|
|
@ -82,7 +82,8 @@ protected:
|
|||
bool IsOnMessageLoopThread();
|
||||
|
||||
ipc::IPCResult RecvGiveBuffer(ipc::Shmem&& aShmem) override;
|
||||
|
||||
ipc::IPCResult RecvPurgeShmems() override;
|
||||
void PurgeShmems();
|
||||
ipc::IPCResult RecvInit(const bool& aAllowDistinctiveIdentifier,
|
||||
const bool& aAllowPersistentState) override;
|
||||
ipc::IPCResult RecvSetServerCertificate(
|
||||
|
|
|
@ -22,7 +22,7 @@ ChromiumCDMParent::ChromiumCDMParent(GMPContentParent* aContentParent,
|
|||
uint32_t aPluginId)
|
||||
: mPluginId(aPluginId)
|
||||
, mContentParent(aContentParent)
|
||||
, mVideoShmemCount(MediaPrefs::EMEChromiumAPIVideoShmemCount())
|
||||
, mVideoShmemLimit(MediaPrefs::EMEChromiumAPIVideoShmemCount())
|
||||
{
|
||||
GMP_LOG(
|
||||
"ChromiumCDMParent::ChromiumCDMParent(this=%p, contentParent=%p, id=%u)",
|
||||
|
@ -600,37 +600,131 @@ ChromiumCDMParent::RecvDecrypted(const uint32_t& aId,
|
|||
return IPC_OK();
|
||||
}
|
||||
|
||||
bool
|
||||
ChromiumCDMParent::PurgeShmems()
|
||||
{
|
||||
GMP_LOG("ChromiumCDMParent::PurgeShmems(this=%p) frame_size=%" PRIuSIZE
|
||||
" limit=%" PRIu32 " active=%" PRIu32,
|
||||
this,
|
||||
mVideoFrameBufferSize,
|
||||
mVideoShmemLimit,
|
||||
mVideoShmemsActive);
|
||||
|
||||
if (mVideoShmemsActive == 0) {
|
||||
// We haven't allocated any shmems, nothing to do here.
|
||||
return true;
|
||||
}
|
||||
if (!SendPurgeShmems()) {
|
||||
return false;
|
||||
}
|
||||
mVideoShmemsActive = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
ChromiumCDMParent::EnsureSufficientShmems(size_t aVideoFrameSize)
|
||||
{
|
||||
GMP_LOG("ChromiumCDMParent::EnsureSufficientShmems(this=%p) "
|
||||
"size=%" PRIuSIZE " expected_size=%" PRIuSIZE " limit=%" PRIu32
|
||||
" active=%" PRIu32,
|
||||
this,
|
||||
aVideoFrameSize,
|
||||
mVideoFrameBufferSize,
|
||||
mVideoShmemLimit,
|
||||
mVideoShmemsActive);
|
||||
|
||||
// The Chromium CDM API requires us to implement a synchronous
|
||||
// interface to supply buffers to the CDM for it to write decrypted samples
|
||||
// into. We want our buffers to be backed by shmems, in order to reduce
|
||||
// the overhead of transferring decoded frames. However due to sandboxing
|
||||
// restrictions, the CDM process cannot allocate shmems itself.
|
||||
// We don't want to be doing synchronous IPC to request shmems from the
|
||||
// content process, nor do we want to have to do intr IPC or make async
|
||||
// IPC conform to the sync allocation interface. So instead we have the
|
||||
// content process pre-allocate a set of shmems and give them to the CDM
|
||||
// process in advance of them being needed.
|
||||
//
|
||||
// When the CDM needs to allocate a buffer for storing a decoded video
|
||||
// frame, the CDM host gives it one of these shmems' buffers. When this
|
||||
// is sent back to the content process, we upload it to a GPU surface,
|
||||
// and send the shmem back to the CDM process so it can reuse it.
|
||||
//
|
||||
// Normally the CDM won't allocate more than one buffer at once, but
|
||||
// we've seen cases where it allocates multiple buffers, returns one and
|
||||
// holds onto the rest. So we need to ensure we have several extra
|
||||
// shmems pre-allocated for the CDM. This threshold is set by the pref
|
||||
// media.eme.chromium-api.video-shmems.
|
||||
//
|
||||
// We also have a failure recovery mechanism; if the CDM asks for more
|
||||
// buffers than we have shmem's available, ChromiumCDMChild gives the
|
||||
// CDM a non-shared memory buffer, and returns the frame to the parent
|
||||
// in an nsTArray<uint8_t> instead of a shmem. Every time this happens,
|
||||
// the parent sends an extra shmem to the CDM process for it to add to the
|
||||
// set of shmems with which to return output. Via this mechanism we should
|
||||
// recover from incorrectly predicting how many shmems to pre-allocate.
|
||||
//
|
||||
// At decoder start up, we guess how big the shmems need to be based on
|
||||
// the video frame dimensions. If we guess wrong, the CDM will follow
|
||||
// the non-shmem path, and we'll re-create the shmems of the correct size.
|
||||
// This meanns we can recover from guessing the shmem size wrong.
|
||||
// We must re-take this path after every decoder de-init/re-init, as the
|
||||
// frame sizes should change every time we switch video stream.
|
||||
|
||||
if (mVideoFrameBufferSize < aVideoFrameSize) {
|
||||
if (!PurgeShmems()) {
|
||||
return false;
|
||||
}
|
||||
mVideoFrameBufferSize = aVideoFrameSize;
|
||||
} else {
|
||||
// Put an upper limit on the number of shmems we tolerate the CDM asking
|
||||
// for, to prevent a memory blow-out. In practice, we expect the CDM to
|
||||
// need less than 5, but some encodings require more.
|
||||
// We'd expect CDMs to not have video frames larger than 720p-1080p
|
||||
// (due to DRM robustness requirements), which is about 1.5MB-3MB per
|
||||
// frame.
|
||||
if (mVideoShmemLimit > 50) {
|
||||
return false;
|
||||
}
|
||||
mVideoShmemLimit++;
|
||||
}
|
||||
|
||||
while (mVideoShmemsActive < mVideoShmemLimit) {
|
||||
if (!SendBufferToCDM(mVideoFrameBufferSize)) {
|
||||
return false;
|
||||
}
|
||||
mVideoShmemsActive++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ipc::IPCResult
|
||||
ChromiumCDMParent::RecvDecodedData(const CDMVideoFrame& aFrame,
|
||||
nsTArray<uint8_t>&& aData)
|
||||
{
|
||||
GMP_LOG("ChromiumCDMParent::RecvDecodedData(this=%p) "
|
||||
"mVideoShmemCount=%" PRIu32,
|
||||
this,
|
||||
mVideoShmemCount);
|
||||
// We'd expect CDMs to not have video frames larger than 1280x720 (due to
|
||||
// DRM robustness requirements), which is about 1.5MB per frame. So put an
|
||||
// upper limit on the number of shmems we tolerate the CDM asking for. In
|
||||
// practice, we expect the CDM to need less than 5, but some encodings
|
||||
// require more.
|
||||
Shmem shmem;
|
||||
if (mVideoShmemCount >= 50 || !AllocShmem(mVideoFrameBufferSize,
|
||||
Shmem::SharedMemory::TYPE_BASIC,
|
||||
&shmem)) {
|
||||
GMP_LOG("ChromiumCDMParent::RecvDecodedData(this=%p) "
|
||||
"failed to allocate shmem for CDM.",
|
||||
this);
|
||||
mVideoDecoderInitialized = false;
|
||||
GMP_LOG("ChromiumCDMParent::RecvDecodedData(this=%p)", this);
|
||||
|
||||
if (mIsShutdown || mDecodePromise.IsEmpty()) {
|
||||
return IPC_OK();
|
||||
}
|
||||
|
||||
if (!EnsureSufficientShmems(aData.Length())) {
|
||||
mDecodePromise.RejectIfExists(
|
||||
MediaResult(
|
||||
NS_ERROR_DOM_MEDIA_FATAL_ERR,
|
||||
RESULT_DETAIL("Failled to send shmems to CDM after decode init.")),
|
||||
MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
|
||||
RESULT_DETAIL("Failled to ensure CDM has enough shmems.")),
|
||||
__func__);
|
||||
return IPC_OK();
|
||||
}
|
||||
mVideoShmemCount++;
|
||||
|
||||
ProcessDecoded(aFrame, aData, Move(shmem));
|
||||
RefPtr<VideoData> v = CreateVideoFrame(aFrame, aData);
|
||||
if (!v) {
|
||||
mDecodePromise.RejectIfExists(
|
||||
MediaResult(NS_ERROR_OUT_OF_MEMORY,
|
||||
RESULT_DETAIL("Can't create VideoData")),
|
||||
__func__);
|
||||
return IPC_OK();
|
||||
}
|
||||
|
||||
mDecodePromise.ResolveIfExists({ Move(v) }, __func__);
|
||||
|
||||
return IPC_OK();
|
||||
}
|
||||
|
@ -639,26 +733,50 @@ ipc::IPCResult
|
|||
ChromiumCDMParent::RecvDecodedShmem(const CDMVideoFrame& aFrame,
|
||||
ipc::Shmem&& aShmem)
|
||||
{
|
||||
ProcessDecoded(
|
||||
aFrame,
|
||||
MakeSpan<uint8_t>(aShmem.get<uint8_t>(), aShmem.Size<uint8_t>()),
|
||||
Move(aShmem));
|
||||
return IPC_OK();
|
||||
}
|
||||
GMP_LOG("ChromiumCDMParent::RecvDecodedShmem(this=%p)", this);
|
||||
|
||||
void
|
||||
ChromiumCDMParent::ProcessDecoded(const CDMVideoFrame& aFrame,
|
||||
Span<uint8_t> aData,
|
||||
ipc::Shmem&& aGiftShmem)
|
||||
{
|
||||
// On failure we need to deallocate the shmem we're to return to the
|
||||
// CDM. On success we return it to the CDM to be reused.
|
||||
auto autoDeallocateShmem =
|
||||
MakeScopeExit([&, this] { this->DeallocShmem(aGiftShmem); });
|
||||
MakeScopeExit([&, this] { this->DeallocShmem(aShmem); });
|
||||
|
||||
if (mIsShutdown || mDecodePromise.IsEmpty()) {
|
||||
return;
|
||||
return IPC_OK();
|
||||
}
|
||||
|
||||
RefPtr<VideoData> v = CreateVideoFrame(
|
||||
aFrame, MakeSpan<uint8_t>(aShmem.get<uint8_t>(), aShmem.Size<uint8_t>()));
|
||||
if (!v) {
|
||||
mDecodePromise.RejectIfExists(
|
||||
MediaResult(NS_ERROR_OUT_OF_MEMORY,
|
||||
RESULT_DETAIL("Can't create VideoData")),
|
||||
__func__);
|
||||
return IPC_OK();
|
||||
}
|
||||
|
||||
// Return the shmem to the CDM so the shmem can be reused to send us
|
||||
// another frame.
|
||||
if (!SendGiveBuffer(aShmem)) {
|
||||
mDecodePromise.RejectIfExists(
|
||||
MediaResult(NS_ERROR_OUT_OF_MEMORY,
|
||||
RESULT_DETAIL("Can't return shmem to CDM process")),
|
||||
__func__);
|
||||
return IPC_OK();
|
||||
}
|
||||
|
||||
// Don't need to deallocate the shmem since the CDM process is responsible
|
||||
// for it again.
|
||||
autoDeallocateShmem.release();
|
||||
|
||||
mDecodePromise.ResolveIfExists({ Move(v) }, __func__);
|
||||
|
||||
return IPC_OK();
|
||||
}
|
||||
|
||||
already_AddRefed<VideoData>
|
||||
ChromiumCDMParent::CreateVideoFrame(const CDMVideoFrame& aFrame,
|
||||
Span<uint8_t> aData)
|
||||
{
|
||||
VideoData::YCbCrBuffer b;
|
||||
MOZ_ASSERT(aData.Length() > 0);
|
||||
|
||||
|
@ -695,27 +813,7 @@ ChromiumCDMParent::ProcessDecoded(const CDMVideoFrame& aFrame,
|
|||
media::TimeUnit::FromMicroseconds(-1),
|
||||
pictureRegion);
|
||||
|
||||
// Return the shmem to the CDM so the shmem can be reused to send us
|
||||
// another frame.
|
||||
if (!SendGiveBuffer(aGiftShmem)) {
|
||||
mDecodePromise.RejectIfExists(
|
||||
MediaResult(NS_ERROR_OUT_OF_MEMORY,
|
||||
RESULT_DETAIL("Can't return shmem to CDM process")),
|
||||
__func__);
|
||||
return;
|
||||
}
|
||||
// Don't need to deallocate the shmem since the CDM process is responsible
|
||||
// for it again.
|
||||
autoDeallocateShmem.release();
|
||||
|
||||
if (v) {
|
||||
mDecodePromise.ResolveIfExists({ Move(v) }, __func__);
|
||||
} else {
|
||||
mDecodePromise.RejectIfExists(
|
||||
MediaResult(NS_ERROR_OUT_OF_MEMORY,
|
||||
RESULT_DETAIL("CallBack::CreateAndCopyData")),
|
||||
__func__);
|
||||
}
|
||||
return v.forget();
|
||||
}
|
||||
|
||||
ipc::IPCResult
|
||||
|
@ -787,7 +885,7 @@ ChromiumCDMParent::InitializeVideoDecoder(
|
|||
__func__);
|
||||
}
|
||||
|
||||
const int32_t bufferSize =
|
||||
const size_t bufferSize =
|
||||
I420FrameBufferSizePadded(aInfo.mImage.width, aInfo.mImage.height);
|
||||
if (bufferSize <= 0) {
|
||||
return MediaDataDecoder::InitPromise::CreateAndReject(
|
||||
|
@ -796,6 +894,13 @@ ChromiumCDMParent::InitializeVideoDecoder(
|
|||
__func__);
|
||||
}
|
||||
|
||||
if (!EnsureSufficientShmems(bufferSize)) {
|
||||
return MediaDataDecoder::InitPromise::CreateAndReject(
|
||||
MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
|
||||
RESULT_DETAIL("Failed to init shmems for video decoder")),
|
||||
__func__);
|
||||
}
|
||||
|
||||
if (!SendInitializeVideoDecoder(aConfig)) {
|
||||
return MediaDataDecoder::InitPromise::CreateAndReject(
|
||||
MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
|
||||
|
@ -822,51 +927,6 @@ ChromiumCDMParent::RecvOnDecoderInitDone(const uint32_t& aStatus)
|
|||
return IPC_OK();
|
||||
}
|
||||
if (aStatus == static_cast<uint32_t>(cdm::kSuccess)) {
|
||||
// The Chromium CDM API requires us to implement a synchronous
|
||||
// interface to supply buffers to the CDM for it to write decrypted samples
|
||||
// into. We want our buffers to be backed by shmems, in order to reduce
|
||||
// the overhead of transferring decoded frames. However due to sandboxing
|
||||
// restrictions, the CDM process cannot allocate shmems itself.
|
||||
// We don't want to be doing synchronous IPC to request shmems from the
|
||||
// content process, nor do we want to have to do intr IPC or make async
|
||||
// IPC conform to the sync allocation interface. So instead we have the
|
||||
// content process pre-allocate a set of shmems and give them to the CDM
|
||||
// process in advance of them being needed.
|
||||
//
|
||||
// When the CDM needs to allocate a buffer for storing a decrypted sample,
|
||||
// the CDM host gives it one of these shmems' buffers. When this is sent
|
||||
// back to the content process, we copy the result out (uploading to a
|
||||
// GPU surface for video frames), and send the shmem back to the CDM
|
||||
// process so it can reuse it.
|
||||
//
|
||||
// We predict the size of buffer the CDM will allocate, and prepopulate
|
||||
// the CDM's list of shmems with shmems of at least that size, plus a bit
|
||||
// of padding for safety.
|
||||
//
|
||||
// Normally the CDM won't allocate more than one buffer at once, but
|
||||
// we've seen cases where it allocates multiple buffers, returns one and
|
||||
// holds onto the rest. So we need to ensure we have a minimum number of
|
||||
// shmems pre-allocated for the CDM. This minimum is set by the pref
|
||||
// media.eme.chromium-api.video-shmems.
|
||||
//
|
||||
// We also have a failure recovery mechanism; if the CDM asks for more
|
||||
// buffers than we have shmem's available, ChromiumCDMChild gives the
|
||||
// CDM a non-shared memory buffer, and returns the frame to the parent
|
||||
// in an nsTArray<uint8_t> instead of a shmem. Every time this happens,
|
||||
// the parent sends an extra shmem to the CDM process for it to add to the
|
||||
// set of shmems with which to return output. Via this mechanism we should
|
||||
// recover from incorrectly predicting how many shmems to pre-allocate.
|
||||
for (uint32_t i = 0; i < mVideoShmemCount; i++) {
|
||||
if (!SendBufferToCDM(mVideoFrameBufferSize)) {
|
||||
mVideoDecoderInitialized = false;
|
||||
mInitVideoDecoderPromise.RejectIfExists(
|
||||
MediaResult(
|
||||
NS_ERROR_DOM_MEDIA_FATAL_ERR,
|
||||
RESULT_DETAIL("Failled to send shmems to CDM after decode init.")),
|
||||
__func__);
|
||||
return IPC_OK();
|
||||
}
|
||||
}
|
||||
mInitVideoDecoderPromise.ResolveIfExists(TrackInfo::kVideoTrack, __func__);
|
||||
} else {
|
||||
mVideoDecoderInitialized = false;
|
||||
|
@ -988,6 +1048,12 @@ ChromiumCDMParent::ShutdownVideoDecoder()
|
|||
return ShutdownPromise::CreateAndResolve(true, __func__);
|
||||
}
|
||||
mVideoDecoderInitialized = false;
|
||||
|
||||
// The ChromiumCDMChild will purge its shmems, so if the decoder is
|
||||
// reinitialized the shmems need to be re-allocated, and they may need
|
||||
// to be a different size.
|
||||
mVideoShmemsActive = 0;
|
||||
mVideoFrameBufferSize = 0;
|
||||
return ShutdownPromise::CreateAndResolve(true, __func__);
|
||||
}
|
||||
|
||||
|
|
|
@ -121,11 +121,6 @@ protected:
|
|||
ipc::Shmem&& aShmem) override;
|
||||
ipc::IPCResult RecvDecodedData(const CDMVideoFrame& aFrame,
|
||||
nsTArray<uint8_t>&& aData) override;
|
||||
|
||||
void ProcessDecoded(const CDMVideoFrame& aFrame,
|
||||
Span<uint8_t> aData,
|
||||
ipc::Shmem&& aGiftShmem);
|
||||
|
||||
ipc::IPCResult RecvDecodeFailed(const uint32_t& aStatus) override;
|
||||
ipc::IPCResult RecvShutdown() override;
|
||||
ipc::IPCResult RecvResetVideoDecoderComplete() override;
|
||||
|
@ -141,6 +136,11 @@ protected:
|
|||
|
||||
bool InitCDMInputBuffer(gmp::CDMInputBuffer& aBuffer, MediaRawData* aSample);
|
||||
|
||||
bool PurgeShmems();
|
||||
bool EnsureSufficientShmems(size_t aVideoFrameSize);
|
||||
already_AddRefed<VideoData> CreateVideoFrame(const CDMVideoFrame& aFrame,
|
||||
Span<uint8_t> aData);
|
||||
|
||||
const uint32_t mPluginId;
|
||||
GMPContentParent* mContentParent;
|
||||
// Note: this pointer is a weak reference because otherwise it would cause
|
||||
|
@ -159,11 +159,13 @@ protected:
|
|||
|
||||
MozPromiseHolder<MediaDataDecoder::FlushPromise> mFlushDecoderPromise;
|
||||
|
||||
int32_t mVideoFrameBufferSize = 0;
|
||||
size_t mVideoFrameBufferSize = 0;
|
||||
|
||||
// Count of the number of shmems in the set used to return decoded video
|
||||
// frames from the CDM to Gecko.
|
||||
uint32_t mVideoShmemCount;
|
||||
uint32_t mVideoShmemsActive = 0;
|
||||
// Maximum number of shmems to use to return decoded video frames.
|
||||
uint32_t mVideoShmemLimit;
|
||||
|
||||
bool mIsShutdown = false;
|
||||
bool mVideoDecoderInitialized = false;
|
||||
|
|
|
@ -239,14 +239,14 @@ GetGMPAbstractThread()
|
|||
return service ? service->GetAbstractGMPThread() : nullptr;
|
||||
}
|
||||
|
||||
static int32_t
|
||||
Align16(int32_t aNumber)
|
||||
static size_t
|
||||
Align16(size_t aNumber)
|
||||
{
|
||||
const uint32_t mask = 15; // Alignment - 1.
|
||||
const size_t mask = 15; // Alignment - 1.
|
||||
return (aNumber + mask) & ~mask;
|
||||
}
|
||||
|
||||
int32_t
|
||||
size_t
|
||||
I420FrameBufferSizePadded(int32_t aWidth, int32_t aHeight)
|
||||
{
|
||||
if (aWidth <= 0 || aHeight <= 0 || aWidth > MAX_VIDEO_WIDTH ||
|
||||
|
@ -254,7 +254,7 @@ I420FrameBufferSizePadded(int32_t aWidth, int32_t aHeight)
|
|||
return 0;
|
||||
}
|
||||
|
||||
int32_t ySize = Align16(aWidth) * Align16(aHeight);
|
||||
size_t ySize = Align16(aWidth) * Align16(aHeight);
|
||||
return ySize + (ySize / 4) * 2;
|
||||
}
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ GetGMPAbstractThread();
|
|||
|
||||
// Returns the number of bytes required to store an aWidth x aHeight image in
|
||||
// I420 format, padded so that the width and height are multiples of 16.
|
||||
int32_t
|
||||
size_t
|
||||
I420FrameBufferSizePadded(int32_t aWidth, int32_t aHeight);
|
||||
|
||||
} // namespace mozilla
|
||||
|
|
|
@ -56,6 +56,8 @@ child:
|
|||
|
||||
async GiveBuffer(Shmem aShmem);
|
||||
|
||||
async PurgeShmems();
|
||||
|
||||
parent:
|
||||
async __delete__();
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче