1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "CanvasDrawEventRecorder.h"
11 #include "mozilla/dom/WorkerCommon.h"
12 #include "mozilla/dom/WorkerPrivate.h"
13 #include "mozilla/dom/WorkerRef.h"
14 #include "mozilla/dom/WorkerRunnable.h"
15 #include "mozilla/layers/TextureRecorded.h"
16 #include "mozilla/layers/SharedSurfacesChild.h"
17 #include "mozilla/StaticPrefs_gfx.h"
18 #include "RecordedCanvasEventImpl.h"
23 struct ShmemAndHandle
{
24 RefPtr
<ipc::SharedMemoryBasic
> shmem
;
28 static Maybe
<ShmemAndHandle
> CreateAndMapShmem(size_t aSize
) {
29 auto shmem
= MakeRefPtr
<ipc::SharedMemoryBasic
>();
30 if (!shmem
->Create(aSize
) || !shmem
->Map(aSize
)) {
34 auto shmemHandle
= shmem
->TakeHandle();
39 return Some(ShmemAndHandle
{shmem
.forget(), std::move(shmemHandle
)});
42 CanvasDrawEventRecorder::CanvasDrawEventRecorder(
43 dom::ThreadSafeWorkerRef
* aWorkerRef
)
44 : mWorkerRef(aWorkerRef
), mIsOnWorker(!!aWorkerRef
) {
45 mDefaultBufferSize
= ipc::SharedMemory::PageAlignedSize(
46 StaticPrefs::gfx_canvas_remote_default_buffer_size());
47 mMaxDefaultBuffers
= StaticPrefs::gfx_canvas_remote_max_default_buffers();
48 mMaxSpinCount
= StaticPrefs::gfx_canvas_remote_max_spin_count();
49 mDropBufferLimit
= StaticPrefs::gfx_canvas_remote_drop_buffer_limit();
50 mDropBufferOnZero
= mDropBufferLimit
;
53 CanvasDrawEventRecorder::~CanvasDrawEventRecorder() { MOZ_ASSERT(!mWorkerRef
); }
55 bool CanvasDrawEventRecorder::Init(TextureType aTextureType
,
56 TextureType aWebglTextureType
,
57 gfx::BackendType aBackendType
,
58 UniquePtr
<Helpers
> aHelpers
) {
59 NS_ASSERT_OWNINGTHREAD(CanvasDrawEventRecorder
);
61 mHelpers
= std::move(aHelpers
);
63 MOZ_ASSERT(mTextureType
== TextureType::Unknown
);
64 auto header
= CreateAndMapShmem(sizeof(Header
));
65 if (NS_WARN_IF(header
.isNothing())) {
69 mHeader
= static_cast<Header
*>(header
->shmem
->memory());
70 mHeader
->eventCount
= 0;
71 mHeader
->writerWaitCount
= 0;
72 mHeader
->writerState
= State::Processing
;
73 mHeader
->processedCount
= 0;
74 mHeader
->readerState
= State::Paused
;
76 // We always keep at least two buffers. This means that when we
77 // have to add a new buffer, there is at least a full buffer that requires
78 // translating while the handle is sent over.
79 AutoTArray
<Handle
, 2> bufferHandles
;
80 auto buffer
= CreateAndMapShmem(mDefaultBufferSize
);
81 if (NS_WARN_IF(buffer
.isNothing())) {
84 mCurrentBuffer
= CanvasBuffer(std::move(buffer
->shmem
));
85 bufferHandles
.AppendElement(std::move(buffer
->handle
));
87 buffer
= CreateAndMapShmem(mDefaultBufferSize
);
88 if (NS_WARN_IF(buffer
.isNothing())) {
91 mRecycledBuffers
.emplace(buffer
->shmem
.forget(), 0);
92 bufferHandles
.AppendElement(std::move(buffer
->handle
));
94 mWriterSemaphore
.reset(CrossProcessSemaphore::Create("CanvasRecorder", 0));
95 auto writerSem
= mWriterSemaphore
->CloneHandle();
96 mWriterSemaphore
->CloseHandle();
97 if (!IsHandleValid(writerSem
)) {
101 mReaderSemaphore
.reset(CrossProcessSemaphore::Create("CanvasTranslator", 0));
102 auto readerSem
= mReaderSemaphore
->CloneHandle();
103 mReaderSemaphore
->CloseHandle();
104 if (!IsHandleValid(readerSem
)) {
108 if (!mHelpers
->InitTranslator(aTextureType
, aWebglTextureType
, aBackendType
,
109 std::move(header
->handle
),
110 std::move(bufferHandles
), mDefaultBufferSize
,
111 std::move(readerSem
), std::move(writerSem
))) {
115 mTextureType
= aTextureType
;
116 mHeaderShmem
= header
->shmem
;
120 void CanvasDrawEventRecorder::RecordEvent(const gfx::RecordedEvent
& aEvent
) {
121 NS_ASSERT_OWNINGTHREAD(CanvasDrawEventRecorder
);
122 aEvent
.RecordToStream(*this);
125 int64_t CanvasDrawEventRecorder::CreateCheckpoint() {
126 NS_ASSERT_OWNINGTHREAD(CanvasDrawEventRecorder
);
127 int64_t checkpoint
= mHeader
->eventCount
;
128 RecordEvent(RecordedCheckpoint());
129 ClearProcessedExternalSurfaces();
133 bool CanvasDrawEventRecorder::WaitForCheckpoint(int64_t aCheckpoint
) {
134 NS_ASSERT_OWNINGTHREAD(CanvasDrawEventRecorder
);
136 uint32_t spinCount
= mMaxSpinCount
;
138 if (mHeader
->processedCount
>= aCheckpoint
) {
141 } while (--spinCount
!= 0);
143 mHeader
->writerState
= State::AboutToWait
;
144 if (mHeader
->processedCount
>= aCheckpoint
) {
145 mHeader
->writerState
= State::Processing
;
149 mHeader
->writerWaitCount
= aCheckpoint
;
150 mHeader
->writerState
= State::Waiting
;
152 // Wait unless we detect the reading side has closed.
153 while (!mHelpers
->ReaderClosed() && mHeader
->readerState
!= State::Failed
) {
154 if (mWriterSemaphore
->Wait(Some(TimeDuration::FromMilliseconds(100)))) {
155 MOZ_ASSERT(mHeader
->processedCount
>= aCheckpoint
);
160 // Either the reader has failed or we're stopping writing for some other
161 // reason (e.g. shutdown), so mark us as failed so the reader is aware.
162 mHeader
->writerState
= State::Failed
;
166 void CanvasDrawEventRecorder::WriteInternalEvent(EventType aEventType
) {
167 MOZ_ASSERT(mCurrentBuffer
.SizeRemaining() > 0);
169 WriteElement(mCurrentBuffer
.Writer(), aEventType
);
170 IncrementEventCount();
173 gfx::ContiguousBuffer
& CanvasDrawEventRecorder::GetContiguousBuffer(
175 if (!mCurrentBuffer
.IsValid()) {
176 // If the current buffer is invalid then we've already failed previously.
177 MOZ_ASSERT(mHeader
->writerState
== State::Failed
);
178 return mCurrentBuffer
;
181 // We make sure that our buffer can hold aSize + 1 to ensure we always have
182 // room for the end of buffer event.
184 // Check if there is enough room is our current buffer.
185 if (mCurrentBuffer
.SizeRemaining() > aSize
) {
186 return mCurrentBuffer
;
189 bool useRecycledBuffer
= false;
190 if (mRecycledBuffers
.front().Capacity() > aSize
) {
191 // The recycled buffer is big enough, check if it is free.
192 if (mRecycledBuffers
.front().eventCount
<= mHeader
->processedCount
) {
193 useRecycledBuffer
= true;
194 } else if (mRecycledBuffers
.size() >= mMaxDefaultBuffers
) {
195 // We've hit he max number of buffers, wait for the next one to be free.
196 // We wait for (eventCount - 1), as we check and signal in the translator
197 // during the play event, before the processedCount has been updated.
198 useRecycledBuffer
= true;
199 if (!WaitForCheckpoint(mRecycledBuffers
.front().eventCount
- 1)) {
200 // The wait failed or we're shutting down, just return an empty buffer.
201 mCurrentBuffer
= CanvasBuffer();
202 return mCurrentBuffer
;
207 if (useRecycledBuffer
) {
208 // Only queue default size buffers for recycling.
209 if (mCurrentBuffer
.Capacity() == mDefaultBufferSize
) {
210 WriteInternalEvent(RECYCLE_BUFFER
);
211 mRecycledBuffers
.emplace(std::move(mCurrentBuffer
.shmem
),
212 mHeader
->eventCount
);
214 WriteInternalEvent(DROP_BUFFER
);
217 mCurrentBuffer
= CanvasBuffer(std::move(mRecycledBuffers
.front().shmem
));
218 mRecycledBuffers
.pop();
220 // If we have more than one recycled buffers free a configured number of
221 // times in a row then drop one.
222 if (mRecycledBuffers
.size() > 1 &&
223 mRecycledBuffers
.front().eventCount
< mHeader
->processedCount
) {
224 if (--mDropBufferOnZero
== 0) {
225 WriteInternalEvent(DROP_BUFFER
);
227 CanvasBuffer(std::move(mRecycledBuffers
.front().shmem
));
228 mRecycledBuffers
.pop();
229 mDropBufferOnZero
= 1;
232 mDropBufferOnZero
= mDropBufferLimit
;
235 return mCurrentBuffer
;
238 // We don't have a buffer free or it is not big enough, so create a new one.
239 WriteInternalEvent(PAUSE_TRANSLATION
);
241 // Only queue default size buffers for recycling.
242 if (mCurrentBuffer
.Capacity() == mDefaultBufferSize
) {
243 mRecycledBuffers
.emplace(std::move(mCurrentBuffer
.shmem
),
244 mHeader
->eventCount
);
247 size_t bufferSize
= std::max(mDefaultBufferSize
,
248 ipc::SharedMemory::PageAlignedSize(aSize
+ 1));
249 auto newBuffer
= CreateAndMapShmem(bufferSize
);
250 if (NS_WARN_IF(newBuffer
.isNothing())) {
251 mHeader
->writerState
= State::Failed
;
252 mCurrentBuffer
= CanvasBuffer();
253 return mCurrentBuffer
;
256 if (!mHelpers
->AddBuffer(std::move(newBuffer
->handle
), bufferSize
)) {
257 mHeader
->writerState
= State::Failed
;
258 mCurrentBuffer
= CanvasBuffer();
259 return mCurrentBuffer
;
262 mCurrentBuffer
= CanvasBuffer(std::move(newBuffer
->shmem
));
263 return mCurrentBuffer
;
266 void CanvasDrawEventRecorder::DropFreeBuffers() {
267 while (mRecycledBuffers
.size() > 1 &&
268 mRecycledBuffers
.front().eventCount
< mHeader
->processedCount
) {
269 // If we encountered an error, we may have invalidated mCurrentBuffer in
270 // GetContiguousBuffer. No need to write the DROP_BUFFER event.
271 if (mCurrentBuffer
.IsValid()) {
272 WriteInternalEvent(DROP_BUFFER
);
274 mCurrentBuffer
= CanvasBuffer(std::move(mRecycledBuffers
.front().shmem
));
275 mRecycledBuffers
.pop();
278 ClearProcessedExternalSurfaces();
281 void CanvasDrawEventRecorder::IncrementEventCount() {
282 mHeader
->eventCount
++;
283 CheckAndSignalReader();
286 void CanvasDrawEventRecorder::CheckAndSignalReader() {
288 switch (mHeader
->readerState
) {
289 case State::Processing
:
293 case State::AboutToWait
:
294 // The reader is making a decision about whether to wait. So, we must
295 // wait until it has decided to avoid races. Check if the reader is
296 // closed to avoid hangs.
297 if (mHelpers
->ReaderClosed()) {
302 if (mHeader
->processedCount
< mHeader
->eventCount
) {
303 // We have to use compareExchange here because the reader can change
304 // from Waiting to Stopped.
305 if (mHeader
->readerState
.compareExchange(State::Waiting
,
306 State::Processing
)) {
307 mReaderSemaphore
->Signal();
311 MOZ_ASSERT(mHeader
->readerState
== State::Stopped
);
316 if (mHeader
->processedCount
< mHeader
->eventCount
) {
317 mHeader
->readerState
= State::Processing
;
318 if (!mHelpers
->RestartReader()) {
319 mHeader
->writerState
= State::Failed
;
324 MOZ_ASSERT_UNREACHABLE("Invalid waiting state.");
330 void CanvasDrawEventRecorder::DetachResources() {
331 NS_ASSERT_OWNINGTHREAD(CanvasDrawEventRecorder
);
333 DrawEventRecorderPrivate::DetachResources();
336 auto lockedPendingDeletions
= mPendingDeletions
.Lock();
337 mWorkerRef
= nullptr;
341 void CanvasDrawEventRecorder::QueueProcessPendingDeletionsLocked(
342 RefPtr
<CanvasDrawEventRecorder
>&& aRecorder
) {
346 "QueueProcessPendingDeletionsLocked called after worker shutdown!");
348 NS_DispatchToMainThread(NS_NewRunnableFunction(
349 "CanvasDrawEventRecorder::QueueProcessPendingDeletionsLocked",
350 [self
= std::move(aRecorder
)]() { self
->ProcessPendingDeletions(); }));
354 if (!NS_IsMainThread()) {
355 NS_DispatchToMainThread(NS_NewRunnableFunction(
356 "CanvasDrawEventRecorder::QueueProcessPendingDeletionsLocked",
357 [self
= std::move(aRecorder
)]() mutable {
358 self
->QueueProcessPendingDeletions(std::move(self
));
363 class ProcessPendingRunnable final
: public dom::WorkerRunnable
{
365 ProcessPendingRunnable(dom::WorkerPrivate
* aWorkerPrivate
,
366 RefPtr
<CanvasDrawEventRecorder
>&& aRecorder
)
367 : dom::WorkerRunnable(aWorkerPrivate
),
368 mRecorder(std::move(aRecorder
)) {}
370 bool WorkerRun(JSContext
*, dom::WorkerPrivate
*) override
{
371 RefPtr
<CanvasDrawEventRecorder
> recorder
= std::move(mRecorder
);
372 recorder
->ProcessPendingDeletions();
377 RefPtr
<CanvasDrawEventRecorder
> mRecorder
;
380 auto task
= MakeRefPtr
<ProcessPendingRunnable
>(mWorkerRef
->Private(),
381 std::move(aRecorder
));
382 if (NS_WARN_IF(!task
->Dispatch())) {
383 MOZ_CRASH("ProcessPendingRunnable leaked!");
387 void CanvasDrawEventRecorder::QueueProcessPendingDeletions(
388 RefPtr
<CanvasDrawEventRecorder
>&& aRecorder
) {
389 auto lockedPendingDeletions
= mPendingDeletions
.Lock();
390 if (lockedPendingDeletions
->empty()) {
391 // We raced to handle the deletions, and something got there first.
395 QueueProcessPendingDeletionsLocked(std::move(aRecorder
));
398 void CanvasDrawEventRecorder::AddPendingDeletion(
399 std::function
<void()>&& aPendingDeletion
) {
400 PendingDeletionsVector pendingDeletions
;
403 auto lockedPendingDeletions
= mPendingDeletions
.Lock();
404 bool wasEmpty
= lockedPendingDeletions
->empty();
405 lockedPendingDeletions
->emplace_back(std::move(aPendingDeletion
));
407 MOZ_RELEASE_ASSERT(!mIsOnWorker
|| mWorkerRef
,
408 "AddPendingDeletion called after worker shutdown!");
410 // If we are not on the owning thread, we must queue an event to run the
411 // deletions, if we transitioned from empty to non-empty.
412 if ((mWorkerRef
&& !mWorkerRef
->Private()->IsOnCurrentThread()) ||
413 (!mWorkerRef
&& !NS_IsMainThread())) {
415 RefPtr
<CanvasDrawEventRecorder
> self(this);
416 QueueProcessPendingDeletionsLocked(std::move(self
));
421 // Otherwise, we can just run all of them right now.
422 pendingDeletions
.swap(*lockedPendingDeletions
);
425 for (const auto& pendingDeletion
: pendingDeletions
) {
430 void CanvasDrawEventRecorder::StoreSourceSurfaceRecording(
431 gfx::SourceSurface
* aSurface
, const char* aReason
) {
432 NS_ASSERT_OWNINGTHREAD(CanvasDrawEventRecorder
);
434 if (NS_IsMainThread()) {
435 wr::ExternalImageId extId
{};
436 nsresult rv
= layers::SharedSurfacesChild::Share(aSurface
, extId
);
437 if (NS_SUCCEEDED(rv
)) {
438 StoreExternalSurfaceRecording(aSurface
, wr::AsUint64(extId
));
439 mExternalSurfaces
.back().mEventCount
= mHeader
->eventCount
;
444 DrawEventRecorderPrivate::StoreSourceSurfaceRecording(aSurface
, aReason
);
447 void CanvasDrawEventRecorder::ClearProcessedExternalSurfaces() {
448 while (!mExternalSurfaces
.empty()) {
449 if (mExternalSurfaces
.front().mEventCount
> mHeader
->processedCount
) {
452 mExternalSurfaces
.pop_front();
456 } // namespace layers
457 } // namespace mozilla